//=====================================================================
// File: ABILaneFilter.java
// Class: ABILaneFilter
// Package: AFLPcore
//
// Author: James J. Benham
// Date: October 16, 2000
// Contact: james_benham@hmc.edu
//
// Genographer v1.6 - Computer assisted scoring of gels.
// Changes Copyright (C) 2001 James J. Benham
// Copyright (C) 1993 Montana State University
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; version 2
// of the License.
//
// 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//
// The GNU General Public License is distributed in the file GPL
//=====================================================================
package AFLPcore;
import java.io.File;
import java.io.RandomAccessFile;
import java.io.IOException;
import java.util.NoSuchElementException;
/**
* This class reads data from a ABI 3100 and a 3700 capillary machine.
* As far as the program is concerned, a capilary is identical to
* a lane in a gel.
*
* The file format for the file is nearly identical to the format for the
* ABI 377 (the ABILaneFilter class), and the description will not be
* duplicated here.
*
* The major difference is that the LANS data does not always seem to be
* present. This means that the color of the size standard must be selected
* by the user.
*
* @see ABILaneFilter
*/
public class ABI3x00Filter extends ImportFilter
{
// Variables from parent class
//private protected int filetype; // the type, see constants above
//private protected String name; // the name of this filter
//private protected String descript; // a brief description
//private protected File helpFile; // represents the file that contains
// the help info for this filter.
// Used to indentify the different entries of interest in the ABI file and
// store the index into an array that contains the info.
private static int NUM_ENTRIES = 10; // the number of entries.
private static final int DATA = 0;
private static final int GELN = 1;
private static final int LANE = 2;
private static final int LANS = 3;
private static final int PEAK1 = 4; // Keep these 4 in order!
private static final int PEAK2 = 5;
private static final int PEAK3 = 6;
private static final int PEAK4 = 7;
private static final int SpNm = 8;
private static final int StdF = 9;
private ABIIndexEntry entries[];
/** color channel */
public static final int YELLOW = 2; /** color channel */
public static final int RED = 3; /** color channel */
public static final int BLUE = 0; /** color channel */
public static final int GREEN = 1;
public static final int ALL = 4;
private int colorChannel=0;
private int stdChannel=0;
private String standardName;
private SizeFunction sizeFn;
/**
* Creates a new filter to read in ABI files.
*/
public ABI3x00Filter()
{
// Initialize the variables for this filter
filetype = LANE;
name = "ABI 3100/3700";
descript = "Reads lane files from ABI 3100 or ABI3700.";
helpFile = "abi3x00.html";
// Options must be set.
options = null;
standardName = "not set";
sizeFn = null;
}
/**
* Access the name of the filter.
*
* @return name of the import filter
*/
public String getName()
{
return name;
}
/**
* Returns the type of input file supported by this filter In this case
* ImportFilter.LANE
, since the filter reads in lane data.
*
* @return constant LANE.
*/
public int getFileType()
{
return filetype;
}
/**
* Retrieves a short, approximately one sentence, description of the filter.
*
* @return the description
*/
public String getDescription()
{
return descript;
}
/**
* The help file describes which files the filter reads and the options
* that this filter accepts.
*
* @return File that contains the help information, either html or
* plaintext.
*/
public String getHelpFile()
{
return helpFile;
}
/**
* Returns the options for this filter, which includes the color of the
* data, the size function to use, and the size standard. The first
* option is the color to read, which can be one of four possilbe
* values: Red, Blue, Green, or Yellow. The color choice is given as
* a Option
of type CHOICE
. The second option
* allows the color of the size standard to be selected. The third
* option is also of type CHOICE
. It tells which size
* method should be used to compute the size of the fragements. Please
* see the help files and the code for the size functions for a
* description of how the work. The third option describes the size
* standard to use. This simply gives the program a list of values.
* These are stored in a file called "standards.cfg" Possible values
* for all of these options are read in from the
* FeatureList
class.
*
* @return an array containing the options described above.
*
* @see Option
* @see FeatureList
* @see SizeFunction
* @see SizeStandard
*/
public Option[] getOptions()
{
Option[] returnOpts = new Option[4];
// Pick the color
String[] colors = new String[5];
colors[RED] = "Red";
colors[BLUE] = "Blue";
colors[GREEN] = "Green";
colors[YELLOW] = "Yellow";
colors[ALL] = "All";
Option param = new Option("Color", Option.CHOICE, true, colors, "Blue");
returnOpts[0] = param;
String [] stdColors = new String[4];
stdColors[RED] = "Red";
stdColors[BLUE] = "Blue";
stdColors[GREEN] = "Green";
stdColors[YELLOW] = "Yellow";
param = new Option("Standard Color", Option.CHOICE, true,
stdColors, "Red");
returnOpts[1] = param;
// The size function option, possiblities retrieved from the
// feature list.
param = new Option("Size Method", Option.CHOICE, true,
FeatureList.getSizeMgr().getNames(),
FeatureList.getSizeMgr().getDefaultName());
returnOpts[2] = param;
// the size standards defined
try {
param = new Option("Size Standard", Option.CHOICE, true,
FeatureList.getStandardMgr().getNames());
} catch(IOException e) {
throw new MissingParameterError("Error accessing standards file. " +
e.getMessage());
}
returnOpts[3] = param;
return returnOpts;
}
/**
* Sets the parameters for the filter to the specified values, including
* color. The color must be set before this filter can run. The option
* representing the color should have a string value naming the color.
* The size function must also be set for the filter to work. It
* must contain the name of a valid SizeFunction
. Note that
* the name is not the class name of the SizeFunction
, but
* the name each SizeFunction
stores internally. The
* third option must also be set.
*
* @param opts an array of length 4 which contains the options
* mentioned above and described in getOptions()
* The order must be: color, size function, size standard.
*
* @exception MissingParameterError occurs when the filter fails to
* extract a string from the first option in opts
.
* @exception IllegalArgumentException occurs when a string is found but
* cannot be matched to one of the colors: Red, Blue, Green, or Yellow.
* Or if an array with length not equal to 3 is given as
* opts
, or if the specified size function, the second
* option, could not be matched to a defined size function.
*/
public void setOptions(Option[] opts)
{
// Check the length.
if(opts.length != 4)
throw new IllegalArgumentException("Invalid options for ABI Lane " +
"Filter. 4 options expected, but " +
opts.length + " were provided.");
// extract the option
String value = opts[0].getStringValue();
// store the options
options = opts;
// check to make sure we have a string
if (value == null)
throw new MissingParameterError("Color not provided as parameter to " +
"ABI 3x00 Filter.");
if(value.equalsIgnoreCase("Red"))
colorChannel = RED;
else if(value.equalsIgnoreCase("Blue"))
colorChannel = BLUE;
else if(value.equalsIgnoreCase("Green"))
colorChannel = GREEN;
else if(value.equalsIgnoreCase("Yellow"))
colorChannel = YELLOW;
else if(value.equalsIgnoreCase("All"))
colorChannel = ALL;
else
{
// didn't match a color, so something is wrong.
// set the options back to null since the ones we got were no good.
options = null;
// and complain
throw new IllegalArgumentException("Invalid color specified for ABI" +
" 3x00 Filter.");
}
// Size standard color
// extract the option
value = opts[1].getStringValue();
// check to make sure we have a string
if (value == null)
throw new MissingParameterError("Color not provided as parameter to " +
"ABI 3x00 Filter.");
if(value.equalsIgnoreCase("Red"))
stdChannel = RED;
else if(value.equalsIgnoreCase("Blue"))
stdChannel = BLUE;
else if(value.equalsIgnoreCase("Green"))
stdChannel = GREEN;
else if(value.equalsIgnoreCase("Yellow"))
stdChannel = YELLOW;
else
{
// didn't match a color, so something is wrong.
// set the options back to null since the ones we got were no good.
options = null;
// and complain
throw new IllegalArgumentException("Invalid color specified for ABI" +
" 3100/3700 Size Standard.");
}
// Next should be the size function
String sizeFnName = opts[2].getStringValue();
try
{
sizeFn = (SizeFunction) FeatureList.getSizeMgr().get(sizeFnName);
}
catch(NoSuchElementException e)
{
options = null;
throw new IllegalArgumentException("Invalid sizing function specified"
+ " for ABI 3x00 Filter. ");
}
// The final option is the size standard definition
standardName = opts[3].getStringValue();
// this will be checked later
}
/**
* This is the method that is called to preform the actual reading of the
* file. The data in the file represents data from a single lane. The
* options/parameters required for the filter should be set using
* setOptions
, and if they are not, an exception will be
* thrown.
*
* @param inputFile The file that contains the lane data.
*
* @return a Lane object with all of the appropriate information.
*
* @exception MissingParameterError occurs if the options are not
* set. Since this includes the required color, the filter cannot
* read in the lane.
* @exception IOException If an error is encountered in the file,
* then this exception will be thrown
*/
public Lane [] readLane(File inputFile) throws IOException
{
Lane newLane;
Lane [] laneArray;
boolean allChannels;
long indexOffset;
long indexLength;
DataList stdPoints;
int peakIndex;
SizeStandard sizeStd;
SizeFunction sizeFn;
// Make sure we have options set, including the color channel
if(options == null)
throw new MissingParameterError("The color for the filter must be set" +
" before the filter can work.");
// Open the file. Set the mode to read only.
RandomAccessFile in = new RandomAccessFile(inputFile, "r");
// Check the file type. They all seem to start with "ABIF", which
// becomes 0x41424946 in hex.
int magicNum = in.readInt();
if( magicNum != 0x41424946)
throw new IOException("This does not appear to be an ABI lane file." +
" See help for more info.");
// Get the length of the index of types.
in.seek(18);
indexLength = (long) in.readInt();
// Get the location of the index.
in.seek(26);
indexOffset = (long) in.readInt();
if(colorChannel == ALL){
allChannels = true;
colorChannel = -1;
laneArray = new Lane[4];
for(int i = 0; i < 4; i++)
laneArray[i] = null;
}
else{
allChannels = false;
laneArray = new Lane[1];
}
// All channels code added by Philip DeCamp
for(int i = 0; i < 4; i++) {
if(allChannels){
for(;;){
colorChannel++;
if(colorChannel > 3)
break;
entries = readRecords(indexOffset, indexLength, in);
if(colorChannel != stdChannel){
try{
checkForColor();
break;
}
catch(IOException e){
// The color is not prsent
// No action needed
}
}
}
if(colorChannel > 3)
break;
}
else {
entries = readRecords(indexOffset, indexLength, in);
}
// Move to the location of the Data
in.seek(entries[DATA].dataOffset);
int traceSize = (int) entries[DATA].numElements;
//System.out.println("Found " + traceSize + " data elements.");
double[] trace = new double[traceSize];
int traceValue;
for(int j= 0; j < entries[DATA].numElements; j++){
// WARNING: We shouldn't get Negative values here, but
// we did (> 32768). Just zero them.
traceValue = in.readUnsignedShort();
trace[j] = (double) ((traceValue > 32768) ? 0 : traceValue);
}
newLane = new Lane(trace);
// Read in the Sample name.
if(entries[SpNm].dataOffset == 0) {
newLane.setName("unknown");
System.out.println("Sample Name not found.");
} else {
if(entries[SpNm].numElements > 4)
newLane.setName(readPString(entries[SpNm].dataOffset, in));
else
newLane.setName(readPString(entries[SpNm].dataOffset));
}
// Read Lane Number
if(entries[LANE].dataOffset == 0 && entries[LANE].numElements == 0) {
//System.out.println("Lane number not found.");
newLane.setLaneNumber(0);
} else {
newLane.setLaneNumber( (int)(entries[LANE].dataOffset >> 16) );
//System.out.println("Lane number: " + newLane.getLaneNumber());
}
peakIndex = PEAK1 + stdChannel;
//System.out.println("Accessing standard peaks for channel " + peakIndex);
//=========== Read in the standard peaks===========
// get the size standard.
try{
sizeStd = (SizeStandard) FeatureList.getStandardMgr().get(standardName);
} catch(NoSuchElementException e) {
throw new IOException("Unknown size standard! '" +
standardName + "'");
}
//System.out.println("Peak Record Length: " + entries[peakIndex].recLength);
long numPeaks = entries[peakIndex].recLength/96;
//System.out.println("Number of Peaks: " + numPeaks);
stdPoints = new DataList();
Peak pk;
for(int j=0; j < numPeaks; j++)
{
in.seek(entries[peakIndex].dataOffset + j*96);
pk = readPeak(in);
//System.out.print(" Reading Peak Location: " + pk.getLocation());
//System.out.print(" Scan: " + pk.getArea());
//System.out.print(" Height: " + pk.getHeight());
if(sizeStd.contains(pk.getLocation())) {
stdPoints.addData(pk);
//System.out.print("*");
}
//System.out.println();
}
// Set the color channel
newLane.setColor(colorChannel);
//================= set the size function ==============
String sizeName = options[2].getStringValue();
sizeFn = (SizeFunction) FeatureList.getSizeMgr().get(sizeName);
sizeFn = (SizeFunction) sizeFn.clone();
sizeFn.init(stdPoints);
sizeFn.setMaxScan(newLane.getNumPoints() - 1);
newLane.setSizeFunction(sizeFn);
//==================clean up=============================
laneArray[i] = newLane;
if(!allChannels)
break;
}
in.close();
if(allChannels){
for(int i = 0; i < 4; i++){
if(laneArray[i] != null){
allChannels = false;
break;
}
}
if(allChannels)
throw new IOException("No Data Channels Could be Found");
}
//System.out.println("Lane Read Completed.");
return laneArray;
}
/**
* This filter does not read gels.
*
* @return Always null
*/
public Gel readGel(File inputFile) throws IOException
{
return null;
}
/**
* Parses the records portion of the ABI file to gather information about
* several different data structures in the file. The important pieces of
* information are, where the data structure is in the file, and how big
* it is. All of the records start with a four charachter ASCII value.
* the records of interest are DATA, which stores the trace information;
* SMPL, which is the name of the sample; GELN, which is the name of the
* gel on which the sample was run; and LANE, which stores the lane
* number of the sample. In some cases, these identifers are repeated.
* For example, a file can have up to 12 DATA entries, but each one has
* a tag number to seperate it. Only one of these contains the information
* we want.
*
* The parser works be converting strings like "DATA" into a long value,
* which is the ASCII representation of the string. It then compares
* this to the first four bytes of each record. On a match, it will
* look at the rest of the record and decide to either keep it or discard
* it. More details are in the code for those interested.
*
* @param indexOffset the location from the beginning of the file to
* the start of the index of records. ie, the location of the first
* record
* @param indexLength the number of records in the index
* @param in the ABI trace file.
*
* @return the records of interest stored in ABIIndexEntry(s). Only
* the portions of the records that are needed are returned.
*
* @exception IOException could come from RandomAccessFile methods or
* from the method itself MODIFY after complition
*/
private ABIIndexEntry[] readRecords(long indexOffset, long indexLength,
RandomAccessFile in)
throws IOException
{
// Create the structure to hold the records for the entries.
ABIIndexEntry record[] = new ABIIndexEntry[NUM_ENTRIES];
// Add the values that we are looking for
// in some cases, we are looking for certain records. For example:
// the correct color channel's data entry is 9 + colorChannel
// which represents the matrix corrected data in the file.
// The file actully contains anywhere from 8-12 DATA records,
// but the tag number determines which one we want.
record[DATA] = new ABIIndexEntry("DATA", 9 + colorChannel);
// Go for the raw data...
//record[DATA] = new ABIIndexEntry("DATA", 1 + colorChannel);
record[GELN] = new ABIIndexEntry("GELN");
record[LANE] = new ABIIndexEntry("LANE");
record[LANS] = new ABIIndexEntry("LANS");
record[PEAK1] = new ABIIndexEntry("PEAK", 1);
record[PEAK2] = new ABIIndexEntry("PEAK", 2);
record[PEAK3] = new ABIIndexEntry("PEAK", 3);
record[PEAK4] = new ABIIndexEntry("PEAK", 4);
record[SpNm] = new ABIIndexEntry("SpNm");
record[StdF] = new ABIIndexEntry("StdF");
// Variables to temporarly hold the record info while we decide if the
// record is valid.
long nameKey;
long tag;
long numElem;
long length;
long offset;
// Go to the start of the index
in.seek(indexOffset);
// Look for records that begin with the name that we're interested in
// and then look at that record more carefully. If it doesn't look valid,
// we won't copy the temporary values to anything permenant.
for(int count=0; count < indexLength; count++)
{
// read in the name
nameKey = (long) in.readInt();
// Read in the other info
tag = (long) in.readInt();
in.skipBytes(4); // skip to the next interesting part
numElem = (long) in.readInt();
length = (long) in.readInt();
offset = (long) in.readInt();
// Now see if the name matches any of the ones we're looking for by
// comparing it to each entry in the array.
for(int i=0; i < NUM_ENTRIES; i++)
{
if(nameKey == record[i].nameKey)
{
// Make sure we have the data record we want.
// If the tag is set, check to see if it matches.
if( (offset != 0) &&
!((record[i].matchTagNumber()) &&
(tag != record[i].tagNumber)))
{
// Make sure the data points to something
// good. There seem to be a lot of entries that
// don't point to anything. For example, some
// files contain multiple SMPL entries, but only
// one of these has a non-null data pointer. If
// the record points to something, store the
// temporary values.
record[i].tagNumber = tag;
record[i].numElements = numElem;
record[i].recLength = length;
record[i].dataOffset = offset;
// we only need to store it once, so don't go through
// the inner loop extra times.
break;
}
} //if (name matches one we're interested in.
} // for(every entry in the record array)
// move to the next record
in.seek(indexOffset + count*28);
} // for every record
return record;
}
/**
* Check to make sure we found the color channel. If we don't throw an
* exception. This could happen because not every file has every channel
* for the processed color data. In this case, the offset will be zero
* since it was never assigned a value.
*
* @exception IOException occurs when the filter cannot find the color
* channel specified with setColorChannel
in the file.
*/
private void checkForColor() throws IOException
{
if( entries[DATA].dataOffset == 0)
{
String errorMsg="";
switch(colorChannel)
{
case RED:
errorMsg = "red";
break;
case BLUE:
errorMsg = "blue";
break;
case GREEN:
errorMsg = "green";
break;
case YELLOW:
errorMsg = "yellow";
break;
}
errorMsg = "Could not find the color " + errorMsg + " in the file.";
throw new IOException(errorMsg);
}
}
/**
* Read in a peak from the file. A peak in the ABI file is 96 bytes
* long. The first 4 bytes are used to store the scan number as 32-bit
* integer. This scan number is different than the one displayed by the
* ABI programs. It is 1000 less, but the number 1000 could vary. 1000 is
* also the value stored in OFFS. The next two bytes are the height, as
* a 16-bit integer. I don't know what the next 12 bytes are. After that,
* the peak area is stored as a 32-bit integer. Skip four bytes again.
* we then have the size of the peak, in bp. This is a IEEE 754 single
* precision float.
*
*
* Value Start Length(bytes) Type * scan 0 4 integer (1000 + this value) * height 4 2 integer * area 18 4 integer * size 26 4 IEEE 754 single-percision float ** * @param in the input source * * @return a peak, with the size/location and height read from the file * and the area set as the scan number, not the area. * * @exception IOException occurs if the file cannot be read. */ public Peak readPeak(RandomAccessFile in) throws IOException { int scan; int height; int area; double size; scan = in.readInt(); height = in.readUnsignedShort(); in.skipBytes(12); area = in.readInt(); in.skipBytes(4); size = in.readFloat(); return new Peak(size, (double) height, (double) scan); } /** * Read in a Pascal type string, where the first byte is the length of * the string, and the rest are the charachters. This is accomplised by * reading in the bytes (unsigned) and converting them into a charachter * array of the correct length, and then turing the character arrray * into a String. * * @param location where the string is in the file, relative to the * beginning of the file. * @param in the file with the information. * * @return the string as a
String
object.
*
* @exception IOException occurs if location
can not
* be reached for some reason.
*/
String readPString(long location, RandomAccessFile in) throws IOException
{
// Move to the correct location
in.seek(location);
// Read in the length and set up the array
int length = in.readUnsignedByte();
char gelname[] = new char[length];
// fill the array.
for(int i=0; i < length; i++)
gelname[i] = (char) in.readUnsignedByte();
return new String(gelname);
}
/**
* This converts a long integer into a string. This is used when the
* dataOffset contains the actual data. This will happen if it is
* an extremely short string, < 3 characters. The format is as follows,
* the bits 24-31 contain the length of the string, and the bits following
* contain the sequence character. It is perhaps easier to think of it
* as 8 bytes. The first four high-order bytes are not used. A long is
* used to store the original 32-bit data so sign wrapping can be avoided.
* of the lower 4 bytes, say 3, 2, 1 and 0. With 0 being the low-order byte,
* the length of the string is stored in byte 3, while the characters are
* stored in 2, 1, and 0 as neccessary.
*
* @param stringBits a data structure matching that specified above.
*
* @return the string contained in the bits
*/
String readPString(long stringBits)
{
int length = (int) (stringBits >>> 24);
char name[] = new char[length];
// fil the array.
for(int i=0; i < length; i++)
name[i] = (char) ((stringBits >>> ( (2 - i)*8 )) & 0x00000000000000ff);
return new String(name);
}
}