package org.broad.igv.sam.lite;
import htsjdk.samtools.SAMRecord;
import htsjdk.samtools.util.StringUtil;
import org.broad.igv.prefs.Constants;
import org.broad.igv.prefs.IGVPreferences;
import org.broad.igv.prefs.PreferencesManager;
import org.broad.igv.sam.*;
import org.broad.igv.sam.ReadMate;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
/**
* Created by jrobinso on 3/13/17.
*/
public class BAMAlignment extends SAMAlignment {
static int READ_PAIRED_FLAG = 0x1;
static int PROPER_PAIR_FLAG = 0x2;
static int READ_UNMAPPED_FLAG = 0x4;
static int MATE_UNMAPPED_FLAG = 0x8;
static int READ_STRAND_FLAG = 0x10;
static int MATE_STRAND_FLAG = 0x20;
static int FIRST_OF_PAIR_FLAG = 0x40;
static int SECOND_OF_PAIR_FLAG = 0x80;
static int SECONDARY_ALIGNMENT_FLAG = 0x100;
static int READ_FAILS_VENDOR_QUALITY_CHECK_FLAG = 0x200;
static int DUPLICATE_READ_FLAG = 0x400;
static int SUPPLEMENTARY_ALIGNMENT_FLAG = 0x800;
public int flags;
public int fragmentLength;
public int lengthOnRef;
public int start;
public byte[] sequence;
public byte[] qualities;
public int mq;
public String readName;
public byte[] tagBytes;
public byte[] cigarBytes;
public String chr;
public ReadMate mate;
private Map<String, Object> tagDictionary;
@Override
public String getReadName() {
return readName;
}
@Override
public int getMappingQuality() {
return mq;
}
@Override
public int getInferredInsertSize() {
return 0;
}
@Override
public String getCigarString() {
return new String(cigarBytes);
}
@Override
public int getReadLength() {
return sequence.length;
}
@Override
public String getReadSequence() {
return new String(sequence);
}
@Override
public boolean isNegativeStrand() {
return (this.flags & READ_STRAND_FLAG) != 0;
}
@Override
public boolean isFirstOfPair() {
return (this.flags & FIRST_OF_PAIR_FLAG) != 0;
}
@Override
public boolean isSecondOfPair() {
return (this.flags & SECOND_OF_PAIR_FLAG) != 0;
}
@Override
public boolean isDuplicate() {
return (this.flags & DUPLICATE_READ_FLAG) != 0;
}
@Override
public boolean isMapped() {
return (this.flags & READ_UNMAPPED_FLAG) == 0;
}
@Override
public boolean isPaired() {
return (this.flags & READ_PAIRED_FLAG) != 0;
}
@Override
public boolean isProperPair() {
return (this.flags & PROPER_PAIR_FLAG) != 0;
}
@Override
public boolean isSupplementary() {
return (this.flags & SUPPLEMENTARY_ALIGNMENT_FLAG) != 0;
}
@Override
public boolean isVendorFailedRead() {
return (this.flags & READ_FAILS_VENDOR_QUALITY_CHECK_FLAG) != 0;
}
@Override
public boolean isPrimary() {
return (this.flags & SECONDARY_ALIGNMENT_FLAG) == 0;
}
@Override
public int getAlignmentStart() {
return start;
}
@Override
public int getAlignmentEnd() {
return start + lengthOnRef;
}
@Override
public Object getAttribute(String key) {
return null;
}
@Override
public String getSample() {
return null;
}
@Override
public String getReadGroup() {
return null;
}
@Override
public String getLibrary() {
return null;
}
@Override
public String getAttributeString(boolean truncate) {
IGVPreferences prefMgr = PreferencesManager.getPreferences(); // TODO -- pass hidden tags as argument
// List of tags to skip. Some tags, like MD and SA, are both quite verbose and not easily
// interpreted by a human reader. It is best to just hide these tags. The list of tags
// to hide is set through the SAM_HIDDEN_TAGS preference.
ArrayList<String> tagsToHide = new ArrayList<String>(),
tagsHidden = new ArrayList<String>();
String samHiddenTagsPref = prefMgr.get(Constants.SAM_HIDDEN_TAGS);
for (String s : (samHiddenTagsPref == null ? "" : samHiddenTagsPref).split("[, ]")) {
if (!s.equals("")) {
tagsToHide.add(s);
}
}
StringBuffer buf = new StringBuffer();
Map<String, Object> attributes = getTagDictionary();
if (attributes != null && !attributes.isEmpty()) {
for (Map.Entry<String, Object> entry: attributes.entrySet()) {
String tag = entry.getKey();
Object value = entry.getValue();
if (tagsToHide.contains(tag)) {
tagsHidden.add(tag);
continue;
}
buf.append("<br>" + tag + " = ");
if (value.getClass().isArray()) { // ignore array types
buf.append("[not shown]<br>");
continue;
}
// Break tag
final String tagValue = value.toString();
final int maxLength = 70;
if (tagValue.length() > maxLength && truncate) {
String[] tokens = tagValue.split("<br>");
for (String token : tokens) {
if (token.length() > maxLength) {
// Insert line breaks
String remainder = token;
while (remainder.length() > maxLength) {
String tmp = remainder.substring(0, maxLength);
int spaceIndex = tmp.lastIndexOf(' ');
int idx = spaceIndex > 30 ? spaceIndex : maxLength;
final String substring = remainder.substring(0, idx);
buf.append(substring);
buf.append("<br>");
remainder = remainder.substring(idx);
}
buf.append(remainder);
buf.append("<br>");
} else {
buf.append(token);
buf.append("<br>");
}
}
} else {
buf.append(tagValue);
}
}
if (tagsHidden.size() > 0) {
buf.append("<br>Hidden tags: " + String.join(", ", tagsHidden));
}
}
return buf.toString();
}
private Map<String, Object> getTagDictionary() {
if (this.tagDictionary == null) {
if (this.tagBytes == null) {
return null;
} else {
this.tagDictionary = decodeTags(this.tagBytes);
}
}
return this.tagDictionary;
}
/**
A [!-~] Printable character
i [-+]?[0-9]+ Signed integer5
f [-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)? Single-precision
oating number
Z [ !-~]+ Printable string, including space
H [0-9A-F]+ Byte array in the Hex format6
B [cCsSiIf](,[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)+ Integer or numeric array
*/
private Map<String, Object> decodeTags(byte[] ba) {
Map<String, Object> tags = new LinkedHashMap<>();
ByteBuffer byteBuffer = ByteBuffer.wrap(ba);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
while (byteBuffer.hasRemaining()) {
int p = byteBuffer.position();
String tag = new String(ba, p, 2);
byteBuffer.position(p+2);
char type = (char) byteBuffer.get();
Object value;
switch (type) {
case 'Z':
value = readNullTerminatedString(byteBuffer);
break;
case 'A':
value = (char)byteBuffer.get();
break;
case 'I':
final long val = byteBuffer.getInt() & 0xffffffffL;
if ( val <= Integer.MAX_VALUE ) {
value = (int)val;
}
else {
value = val;
}
break;
case 'i':
value = byteBuffer.getInt();
break;
case 's':
value = byteBuffer.getShort();
break;
case 'S':
// Convert to unsigned short stored in an int
value = (int) byteBuffer.getShort() & 0xffff;
break;
case 'c':
value = byteBuffer.get();
break;
case 'C':
// Convert to unsigned byte stored in an int
value = (int)byteBuffer.get() & 0xff;
break;
case 'f':
value = byteBuffer.getFloat();
break;
case 'H':
final String hexRep = readNullTerminatedString(byteBuffer);
value = StringUtil.hexStringToBytes(hexRep);
break;
case 'B':
value = readArray(byteBuffer);
break;
default:
value = "Unrecognized tag type: " + type;
}
tags.put(tag, value);
}
return tags;
}
private static String readNullTerminatedString(final ByteBuffer byteBuffer) {
// Count the number of bytes in the string
byteBuffer.mark();
final int startPosition = byteBuffer.position();
while (byteBuffer.get() != 0) {}
final int endPosition = byteBuffer.position();
// Don't count null terminator
final byte[] buf = new byte[endPosition - startPosition - 1];
// Go back to the start of the string and read out the bytes
byteBuffer.reset();
byteBuffer.get(buf);
// Skip over the null terminator
byteBuffer.get();
return StringUtil.bytesToString(buf);
}
private static Object readArray(final ByteBuffer byteBuffer) {
final byte arrayType = byteBuffer.get();
final boolean isUnsigned = Character.isUpperCase(arrayType);
final int length = byteBuffer.getInt();
final Object value;
switch (Character.toLowerCase(arrayType)) {
case 'c': {
final byte[] array = new byte[length];
value = array;
byteBuffer.get(array);
break;
}
case 's': {
final short[] array = new short[length];
value = array;
for (int i = 0; i < length; ++i) {
array[i] = byteBuffer.getShort();
}
break;
}
case 'i': {
final int[] array = new int[length];
value = array;
for (int i = 0; i < length; ++i) {
array[i] = byteBuffer.getInt();
}
break;
}
case 'f': {
final float[] array = new float[length];
value = array;
for (int i = 0; i < length; ++i) {
array[i] = byteBuffer.getFloat();
}
break;
}
default:
throw new RuntimeException("Unrecognized tag array type: " + (char)arrayType);
}
return value;
}
}