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; } }