/*
* The MIT License
*
* Copyright (c) 2012 The Broad Institute
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package picard.illumina.parser;
import htsjdk.samtools.util.CloserUtil;
import htsjdk.samtools.util.StringUtil;
import picard.PicardException;
import picard.util.UnsignedTypeUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
/**
* Read a .cnf (binary noise) or .cif (binary intensity) file. A file in this format contains
* 1 or more cycles of data for a set of clusters, with 4 values per cycle, one for each channel.
* A file can store its values in either a byte or a short per value, but the API treats them all as shorts.
* This class does not distinguish btw CIF and CNF files.
*
* @author jburke@broadinstitute.org
*/
class ClusterIntensityFileReader {
private static final byte[] IDENTIFIER = StringUtil.stringToBytes("CIF");
private static final byte FILE_VERSION = 1;
private static final int HEADER_SIZE = 13;
private static final int NUM_CHANNELS = IntensityChannel.values().length;
// Just for error reporting
private final File file;
/**
* The entire file is mmapped
*/
private final MappedByteBuffer buf;
private final ClusterIntensityFileHeader header;
// Precomputed for speed, I hope.
private final int cycleSize;
private final int channelSize;
public static class ClusterIntensityFileHeader {
public final int elementSize;
public final int firstCycle;
public final int numCycles;
public final int numClusters;
public ClusterIntensityFileHeader(final byte[] headerBytes, final File file) {
if(headerBytes.length < HEADER_SIZE) {
throw new PicardException("Bytes past to header constructor are too short excpected(" + HEADER_SIZE + ") received (" + headerBytes.length);
}
ByteBuffer buf = ByteBuffer.allocate(headerBytes.length); //for doing some byte conversions
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put(headerBytes);
buf.position(0);
final byte[] identifierBuf = new byte[IDENTIFIER.length];
buf.get(identifierBuf);
if (!Arrays.equals(identifierBuf, IDENTIFIER)) {
throw new PicardException("Cluster intensity file " + file + " contains unexpected header: " +
StringUtil.bytesToString(identifierBuf));
}
final byte fileVersion = buf.get();
if (fileVersion != FILE_VERSION) {
throw new PicardException("Cluster intensity file " + file + " contains unexpected version: " + fileVersion);
}
elementSize = buf.get();
if (elementSize < 1 || elementSize > 2) {
throw new PicardException("Cluster intensity file " + file + " contains unexpected element size: " + elementSize);
}
// convert these to unsigned
firstCycle = UnsignedTypeUtil.uShortToInt(buf.getShort());
numCycles = UnsignedTypeUtil.uShortToInt(buf.getShort());
if (numCycles == 0) {
throw new PicardException("Cluster intensity file " + file + " has zero cycles.");
}
numClusters = buf.getInt();
if (numClusters < 0) {
// It is possible for there to be no clusters in a tile.
throw new PicardException("Cluster intensity file " + file + " has negative number of clusters: " +numClusters);
}
}
}
/**
* Prepare to parse a CIF or CNF file.
* @param file The file to be parsed.
*/
public ClusterIntensityFileReader(final File file) {
try {
this.file = file;
final FileInputStream is = new FileInputStream(this.file);
final FileChannel channel = is.getChannel();
final long fileSize = channel.size();
buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
buf.order(ByteOrder.LITTLE_ENDIAN);
CloserUtil.close(channel);
CloserUtil.close(is);
final byte [] headerBytes = new byte[HEADER_SIZE];
buf.get(headerBytes);
this.header = new ClusterIntensityFileHeader(headerBytes, this.file);
} catch (IOException e) {
throw new PicardException("IOException opening cluster intensity file " + file, e);
}
cycleSize = NUM_CHANNELS * header.numClusters * header.elementSize;
channelSize = header.numClusters * header.elementSize;
}
/**
* Get the value for the given args. Value is returned as a signed short regardless of whether storage is
* in bytes or shorts.
* @param cluster 0-based cluster number.
* @param channel Which channel is desired.
* @param cycle Absolute cycle number. E.g. if the first cycle in the file is N, then the first value that can
* be fetched is cycle=N
* @return Intensity or noise (depending on whether this is a CIF or CNF file).
*/
public short getValue(final int cluster, final IntensityChannel channel, final int cycle) {
if (cycle < header.firstCycle || cycle >= header.firstCycle + header.numCycles) {
throw new IllegalArgumentException("Requested cycle (" + cycle + ") number out of range. First cycle=" +
header.firstCycle + "; numCycles=" + header.numCycles);
}
if (cluster < 0 || cluster >= header.numClusters) {
throw new IllegalArgumentException("Requested cluster (" + cluster + ") number out of range. numClustersInTile=" + header.numClusters);
}
final int relativeCycle = cycle - header.firstCycle;
final int position = HEADER_SIZE + relativeCycle * cycleSize + channel.ordinal() * channelSize + cluster * header.elementSize;
buf.position(position);
if (header.elementSize == 1) {
return buf.get();
} else {
return buf.getShort();
}
}
public File getFile() {
return file;
}
/**
* @return The first (one-based) cycle stored in this file.
*/
public int getFirstCycle() {
return header.firstCycle;
}
/**
* @return Number of clusters stored in this file.
*/
public int getNumClusters() {
return header.numClusters;
}
/**
* @return Number of cycles stored in this file.
*/
public int getNumCycles() {
return header.numCycles;
}
/**
* @return the size of one intensity value for one channel in this file.
*/
public int getElementSize() {
return header.elementSize;
}
public static ClusterIntensityFileHeader readHeaders(final File intensityFile) {
FileInputStream reader = null;
byte [] headerBytes = new byte[HEADER_SIZE];
int bytesRead = 0;
try {
reader = new FileInputStream(intensityFile);
bytesRead = reader.read(headerBytes);
} catch(FileNotFoundException fnfExc) {
throw new PicardException("Error opening intensity file (" + intensityFile.getAbsolutePath() +")", fnfExc);
} catch(IOException ioExc) {
throw new PicardException("Error reading values from header for intensity file (" + intensityFile.getAbsolutePath() + ")", ioExc);
} finally {
CloserUtil.close(reader);
}
if(bytesRead != HEADER_SIZE)
throw new PicardException("Error reading intensity file header, too few bytes read, expected( " + HEADER_SIZE + ") read(" + bytesRead + ")");
return new ClusterIntensityFileHeader(headerBytes, intensityFile);
}
}