/*
* The MIT License
*
* Copyright (c) 2013 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.sam;
import htsjdk.samtools.SAMRecord;
import htsjdk.samtools.SAMTag;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Holds all the hits (alignments) for a read or read pair. For single-end reads, all the alignments are
* stored in firstOfPairOrFragment list. For paired-end, alignments are stored in firstOfPairOrFragment list and
* secondOfPair list.
*
* If there is more than one alignment, the selected PrimaryAlignmentSelectionStrategy is used to decide which
* alignment should be marked as primary. The rest are marked as secondary.
*
* When AbstractAlignmentMerger emits these reads, for paired end reads it assumes that the ith first end alignment
* and the ith second end alignment are correlated for the purpose of setting mate information in the SAMRecord.
* If it is not appropriate for the ends to be linked like that, then the alignments should be staggered in
* the lists so that there is a null in the other end list for each alignment. E.g. for the firstOfPair(5),
* secondOfPair(5) should be null in order not to set the mate info. In that case the mate info will indicate that
* the other end is unmapped.
*/
class HitsForInsert {
private static final HitIndexComparator comparator = new HitIndexComparator();
public enum NumPrimaryAlignmentState {
NONE, ONE, MORE_THAN_ONE
}
// These are package-visible to make life easier for the PrimaryAlignmentSelectionStrategies.
final List<SAMRecord> firstOfPairOrFragment = new ArrayList<SAMRecord>();
final List<SAMRecord> secondOfPair = new ArrayList<SAMRecord>();
private final List<SAMRecord> supplementalFirstOfPairOrFragment = new ArrayList<SAMRecord>();
private final List<SAMRecord> supplementalSecondOfPair = new ArrayList<SAMRecord>();
/**
* @throws if numHits() == 0
*/
public String getReadName() {
return getRepresentativeRead().getReadName();
}
/**
* @throws if numHits() == 0
*/
public boolean isPaired() {
return getRepresentativeRead().getReadPairedFlag();
}
public SAMRecord getRepresentativeRead() {
for (final SAMRecord rec : firstOfPairOrFragment) {
if (rec != null) return rec;
}
for (final SAMRecord rec : secondOfPair) {
if (rec != null) return rec;
}
throw new IllegalStateException("Should not be called if numHits == 0");
}
/**
* Note that a single alignment for each end of a read pair is counted as a single hit.
*/
public int numHits() {
return Math.max(firstOfPairOrFragment.size(), secondOfPair.size());
}
/** True if either the first or second of pair has supplementary alignments, otherwise false. */
public boolean hasSupplementalHits() {
return !(this.supplementalFirstOfPairOrFragment.isEmpty() && this.supplementalSecondOfPair.isEmpty());
}
/**
* @return Returns the ith hit for the first end, or null if the first end is not aligned.
*/
public SAMRecord getFirstOfPair(final int i) {
if (i >= firstOfPairOrFragment.size()) {
return null;
} else {
return firstOfPairOrFragment.get(i);
}
}
public void addFirstOfPairOrFragment(final SAMRecord rec) {
firstOfPairOrFragment.add(rec);
}
public void addSecondOfPair(final SAMRecord rec) {
secondOfPair.add(rec);
}
public void addSupplementalFirstOfPairOrFragment(final SAMRecord rec) {
supplementalFirstOfPairOrFragment.add(rec);
}
public void addSupplementalSecondOfPair(final SAMRecord rec) {
supplementalSecondOfPair.add(rec);
}
/**
* @return The ith hit for a un-paired read. Never returns null.
* Do not call if paired read.
*/
public SAMRecord getFragment(final int i) {
final SAMRecord samRecord = firstOfPairOrFragment.get(i);
if (samRecord.getReadPairedFlag()) throw new UnsupportedOperationException("getFragment called for paired read");
return samRecord;
}
/**
* @return Returns the ith hit for the second end, or null if the second end is not aligned.
*/
public SAMRecord getSecondOfPair(final int i) {
if (i >= secondOfPair.size()) {
return null;
} else {
return secondOfPair.get(i);
}
}
/**
* Set all alignments to not primary, except for the one specified by the argument. If paired, and set the
* alignment for both ends if there is an alignment for both ends, otherwise just for the end for which
* there is an alignment at the given index.
* @param primaryAlignmentIndex
*/
public void setPrimaryAlignment(final int primaryAlignmentIndex) {
if (primaryAlignmentIndex < 0 || primaryAlignmentIndex >= this.numHits()) {
throw new IllegalArgumentException("primaryAlignmentIndex(" + primaryAlignmentIndex +
") out of range for numHits(" + numHits() + ")");
}
// Set all alignment to be not primary except the selected one.
for (int i = 0; i < this.numHits(); ++i) {
final boolean notPrimary = (i != primaryAlignmentIndex);
if (this.getFirstOfPair(i) != null) {
this.getFirstOfPair(i).setNotPrimaryAlignmentFlag(notPrimary);
}
if (this.getSecondOfPair(i) != null) {
this.getSecondOfPair(i).setNotPrimaryAlignmentFlag(notPrimary);
}
}
}
/**
* Some alignment strategies expect to receive alignments for ends that are coordinated by
* hit index (HI) tag. This method lines up alignments for each end by HI tag value, and if there is
* no corresponding alignment for an alignment, there is a null in the array at that slot.
*
* This method then renumbers the HI values so that they start at zero and have no gaps, because some
* reads may have been filtered out.
*/
public void coordinateByHitIndex() {
// Sort by HI value, with reads with no HI going at the end.
Collections.sort(firstOfPairOrFragment, comparator);
Collections.sort(secondOfPair, comparator);
// Insert nulls as necessary in the two lists so that correlated alignments have the same index
// and uncorrelated alignments have null in the other list at the corresponding index.
for (int i = 0; i < Math.min(firstOfPairOrFragment.size(), secondOfPair.size()); ++i) {
final Integer leftHi = firstOfPairOrFragment.get(i).getIntegerAttribute(SAMTag.HI.name());
final Integer rightHi = secondOfPair.get(i).getIntegerAttribute(SAMTag.HI.name());
if (leftHi != null) {
if (rightHi != null) {
if (leftHi < rightHi) secondOfPair.add(i, null);
else if (rightHi < leftHi) firstOfPairOrFragment.add(i, null);
// else the are correlated
}
} else if (rightHi != null) {
firstOfPairOrFragment.add(i, null);
} else {
// Both alignments do not have HI, so push down the ones on the right.
// Right is arbitrarily picked to push down.
secondOfPair.add(i, null);
}
}
// Now renumber any correlated alignments, and remove hit index if no correlated read.
int hi = 0;
for (int i = 0; i < numHits(); ++i) {
final SAMRecord first = getFirstOfPair(i);
final SAMRecord second = getSecondOfPair(i);
if (first != null && second != null) {
first.setAttribute(SAMTag.HI.name(), i);
second.setAttribute(SAMTag.HI.name(), i);
++hi;
} else if (first != null) {
first.setAttribute(SAMTag.HI.name(), null);
} else {
second.setAttribute(SAMTag.HI.name(), null);
}
}
}
/**
* Determine if there is a single primary alignment in a list of alignments.
* @param records
* @return NONE, ONE or MORE_THAN_ONE.
*/
private NumPrimaryAlignmentState tallyPrimaryAlignments(final List<SAMRecord> records) {
boolean seenPrimary = false;
for (int i = 0; i < records.size(); ++i) {
if (records.get(i) != null && !records.get(i).isSecondaryOrSupplementary()) {
if (seenPrimary) return NumPrimaryAlignmentState.MORE_THAN_ONE;
else seenPrimary = true;
}
}
if (seenPrimary) return NumPrimaryAlignmentState.ONE;
else return NumPrimaryAlignmentState.NONE;
}
public NumPrimaryAlignmentState tallyPrimaryAlignments(final boolean firstEnd) {
if (firstEnd) return tallyPrimaryAlignments(firstOfPairOrFragment);
else return tallyPrimaryAlignments(secondOfPair);
}
int findPrimaryAlignment(final List<SAMRecord> records) {
int indexOfPrimaryAlignment = -1;
for (int i = 0; i < records.size(); ++i) {
if (records.get(i) != null && !records.get(i).isSecondaryOrSupplementary()) {
if (indexOfPrimaryAlignment != -1) {
throw new IllegalStateException("Multiple primary alignments found for read " + getReadName());
}
indexOfPrimaryAlignment = i;
}
}
return indexOfPrimaryAlignment;
}
// null HI tag sorts after any non-null.
private static class HitIndexComparator implements Comparator<SAMRecord> {
public int compare(final SAMRecord rec1, final SAMRecord rec2) {
final Integer hi1 = rec1.getIntegerAttribute(SAMTag.HI.name());
final Integer hi2 = rec2.getIntegerAttribute(SAMTag.HI.name());
if (hi1 == null) {
if (hi2 == null) return 0;
else return 1;
} else if (hi2 == null) {
return -1;
} else {
return hi1.compareTo(hi2);
}
}
}
List<SAMRecord> getSupplementalFirstOfPairOrFragment() {
return supplementalFirstOfPairOrFragment;
}
List<SAMRecord> getSupplementalSecondOfPair() {
return supplementalSecondOfPair;
}
}