/*
* The MIT License (MIT)
*
* Copyright (c) 2007-2015 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 org.broad.igv.sam;
import org.apache.log4j.Logger;
import org.broad.igv.feature.Range;
import org.broad.igv.feature.genome.Genome;
import org.broad.igv.prefs.IGVPreferences;
import org.broad.igv.prefs.PreferencesManager;
import org.broad.igv.sam.AlignmentTrack.SortOption;
import org.broad.igv.sam.reader.AlignmentReaderFactory;
import org.broad.igv.track.RenderContext;
import org.broad.igv.event.DataLoadedEvent;
import org.broad.igv.event.IGVEventBus;
import org.broad.igv.event.IGVEventObserver;
import org.broad.igv.ui.panel.FrameManager;
import org.broad.igv.ui.panel.ReferenceFrame;
import org.broad.igv.util.ResourceLocator;
import java.io.IOException;
import java.util.*;
import static org.broad.igv.prefs.Constants.*;
public class AlignmentDataManager implements IGVEventObserver {
private static Logger log = Logger.getLogger(AlignmentDataManager.class);
public enum ExperimentType {OTHER, RNA, BISULFITE, THIRD_GEN}
;
private Map<ReferenceFrame, AlignmentInterval> intervalCache;
private ResourceLocator locator;
private HashMap<String, String> chrMappings = new HashMap();
private Set<Range> isLoading = new HashSet<>();
private AlignmentTileLoader reader;
private CoverageTrack coverageTrack;
private Map<String, PEStats> peStats;
private SpliceJunctionHelper.LoadOptions loadOptions;
private Object loadLock = new Object();
private boolean showAlignments = true;
private ExperimentType type = null;
public AlignmentDataManager(ResourceLocator locator, Genome genome) throws IOException {
this.locator = locator;
reader = new AlignmentTileLoader(AlignmentReaderFactory.getReader(locator));
peStats = new HashMap();
initLoadOptions();
initChrMap(genome);
intervalCache = Collections.synchronizedMap(new HashMap<>());
IGVEventBus.getInstance().subscribe(FrameManager.ChangeEvent.class, this);
}
public void receiveEvent(Object event) {
if (event instanceof FrameManager.ChangeEvent) {
Collection<ReferenceFrame> frames = ((FrameManager.ChangeEvent) event).getFrames();
Map<ReferenceFrame, AlignmentInterval> newCache = Collections.synchronizedMap(new HashMap<>());
// Trim cache to include only current frames
for (ReferenceFrame f : frames) {
if (intervalCache.containsKey(f)) {
newCache.put(f, intervalCache.get(f));
}
}
intervalCache = newCache;
} else {
log.info("Unknown event type: " + event.getClass());
}
}
void initLoadOptions() {
this.loadOptions = new SpliceJunctionHelper.LoadOptions();
}
/**
* Create an alias -> chromosome lookup map. Enable loading BAM files that use alternative names for chromosomes,
* provided the alias has been defined (e.g. 1 -> chr1, etc).
*/
private void initChrMap(Genome genome) throws IOException {
if (genome != null) {
List<String> seqNames = reader.getSequenceNames();
if (seqNames != null) {
for (String chr : seqNames) {
String alias = genome.getCanonicalChrName(chr);
chrMappings.put(alias, chr);
}
}
}
}
public AlignmentTileLoader getReader() {
return reader;
}
public ResourceLocator getLocator() {
return locator;
}
public Map<String, PEStats> getPEStats() {
return peStats;
}
public boolean isPairedEnd() {
return reader.isPairedEnd();
}
public boolean hasIndex() {
return reader.hasIndex();
}
public void setType(ExperimentType type) {
if (type != this.type) {
ExperimentTypeChangeEvent event = new ExperimentTypeChangeEvent(this, type);
this.type = type;
IGVEventBus.getInstance().post(event);
}
}
public ExperimentType getType() {
return type;
}
public void setCoverageTrack(CoverageTrack coverageTrack) {
this.coverageTrack = coverageTrack;
}
public CoverageTrack getCoverageTrack() {
return coverageTrack;
}
public double getMinVisibleScale() {
IGVPreferences prefs = PreferencesManager.getPreferences();
float maxRange = prefs.getAsFloat(SAM_MAX_VISIBLE_RANGE);
return (maxRange * 1000) / 700;
}
/**
* The set of sequences found in the file.
* May be null
*
* @return
*/
public List<String> getSequenceNames() throws IOException {
return reader.getSequenceNames();
}
public AlignmentInterval getLoadedInterval(ReferenceFrame frame) {
return intervalCache.get(frame);
}
/**
* Sort rows group by group
*
* @param option
* @param location
*/
public boolean sortRows(SortOption option, ReferenceFrame frame, double location, String tag) {
AlignmentInterval interval = getLoadedInterval(frame);
if (interval == null) {
return false;
} else {
PackedAlignments packedAlignments = interval.getPackedAlignments();
if (packedAlignments == null) {
return false;
}
for (List<Row> alignmentRows : packedAlignments.values()) {
for (Row row : alignmentRows) {
row.updateScore(option, location, interval, tag);
}
Collections.sort(alignmentRows);
}
return true;
}
}
public void setViewAsPairs(boolean option, AlignmentTrack.RenderOptions renderOptions) {
if (option == renderOptions.isViewPairs()) {
return;
}
renderOptions.setViewPairs(option);
packAlignments(renderOptions);
}
/**
* Repack currently loaded alignments across frames
* All relevant intervals must be loaded
*
* @param renderOptions
* @return Whether repacking was performed
*/
void packAlignments(AlignmentTrack.RenderOptions renderOptions) {
for (AlignmentInterval interval : intervalCache.values()) {
interval.packAlignments(renderOptions);
}
}
public boolean isLoaded(ReferenceFrame frame) {
AlignmentInterval interval = intervalCache.get(frame);
if (interval == null) {
return false;
} else {
Range range = frame.getCurrentRange();
return interval.contains(range.getChr(), range.getStart(), range.getEnd());
}
}
public boolean isLoading(ReferenceFrame frame) {
Range range = frame.getCurrentRange();
for(Range r : isLoading) {
if(r.contains(range)) return true;
}
return false;
}
public void load(ReferenceFrame referenceFrame,
AlignmentTrack.RenderOptions renderOptions,
boolean expandEnds) {
if (isLoaded(referenceFrame)) return; // Already loaded
if(isLoading(referenceFrame)) return; // Already oading
synchronized (loadLock) {
Range range = referenceFrame.getCurrentRange();
isLoading.add(range);
final String chr = referenceFrame.getChrName();
final int start = (int) range.getStart();
final int end = (int) range.getEnd();
int adjustedStart = start;
int adjustedEnd = end;
// Expand the interval by the lesser of +/- a 2 screens, or max visible range
int windowSize = Math.min(4 * (end - start), PreferencesManager.getPreferences().getAsInt(SAM_MAX_VISIBLE_RANGE) * 1000);
int center = (end + start) / 2;
int expand = Math.max(end - start, windowSize / 2);
if (expandEnds) {
adjustedStart = Math.max(0, Math.min(start, center - expand));
adjustedEnd = Math.max(end, center + expand);
}
log.debug("Loading alignments: " + chr + ":" + adjustedStart + "-" + adjustedEnd + " for " + AlignmentDataManager.this);
AlignmentInterval loadedInterval = loadInterval(chr, adjustedStart, adjustedEnd, renderOptions);
intervalCache.put(referenceFrame, loadedInterval);
packAlignments(renderOptions);
isLoading.remove(range);
// IGVEventBus.getInstance().post(new DataLoadedEvent(referenceFrame));
}
}
AlignmentInterval loadInterval(String chr, int start, int end, AlignmentTrack.RenderOptions renderOptions) {
String sequence = chrMappings.containsKey(chr) ? chrMappings.get(chr) : chr;
DownsampleOptions downsampleOptions = new DownsampleOptions();
final AlignmentTrack.BisulfiteContext bisulfiteContext =
renderOptions != null ? renderOptions.bisulfiteContext : null;
SpliceJunctionHelper spliceJunctionHelper = new SpliceJunctionHelper(this.loadOptions);
ReadStats readStats = new ReadStats();
AlignmentTileLoader.AlignmentTile t = reader.loadTile(sequence, start, end, spliceJunctionHelper,
downsampleOptions, readStats, peStats, bisulfiteContext, showAlignments);
if (type == null) {
readStats.compute();
inferType(readStats);
}
List<Alignment> alignments = t.getAlignments();
List<DownsampledInterval> downsampledIntervals = t.getDownsampledIntervals();
return new AlignmentInterval(chr, start, end, alignments, t.getCounts(), spliceJunctionHelper, downsampledIntervals);
}
/**
* Some empirical metrics for determining experiment type
*
* @param readStats
*/
private void inferType(ReadStats readStats) {
if (readStats.readLengthStdDev > 100 || readStats.medianReadLength > 1000) {
setType(ExperimentType.THIRD_GEN); // Could also use fracReadsWithIndels
} else if (readStats.medianRefToReadRatio > 10) {
setType(ExperimentType.RNA);
} else {
setType(ExperimentType.OTHER);
}
}
public synchronized PackedAlignments getGroups(RenderContext context, AlignmentTrack.RenderOptions renderOptions) {
// load(context.getReferenceFrame(), renderOptions, false);
// Range range = context.getReferenceFrame().getCurrentRange();
AlignmentInterval interval = intervalCache.get(context.getReferenceFrame());
if (interval != null) {
return interval.getPackedAlignments();
} else {
return null;
}
}
public void clear() {
intervalCache.clear();
}
public void dumpAlignments() {
for (AlignmentInterval interval : intervalCache.values()) {
interval.dumpAlignments();
}
}
/**
* Find the first loaded interval for the specified chromosome and genomic {@code positon},
* return the grouped alignments
*
* @param position
* @param referenceFrame
* @return alignmentRows, grouped and ordered by key
*/
public PackedAlignments getGroupedAlignmentsContaining(double position, ReferenceFrame referenceFrame) {
String chr = referenceFrame.getChrName();
int start = (int) position;
int end = start + 1;
AlignmentInterval interval = intervalCache.get(referenceFrame);
if (interval == null) {
return null;
} else {
PackedAlignments packedAlignments = interval.getPackedAlignments();
if (packedAlignments != null && packedAlignments.contains(chr, start, end)) {
return packedAlignments;
} else {
return null;
}
}
}
public int getNLevels() {
int nLevels = 0;
for (AlignmentInterval interval : intervalCache.values()) {
PackedAlignments packedAlignments = interval.getPackedAlignments();
if (packedAlignments != null) {
int intervalNLevels = packedAlignments.getNLevels();
nLevels = Math.max(nLevels, intervalNLevels);
}
}
return nLevels;
}
/**
* Get the maximum group count among all the loaded intervals. Normally there is one interval, but there
* can be multiple if viewing split screen.
*/
public int getMaxGroupCount() {
int groupCount = 0;
for (AlignmentInterval interval : intervalCache.values()) {
if (interval != null) { // Not sure how this happens but it does
PackedAlignments packedAlignments = interval.getPackedAlignments();
if (packedAlignments != null) {
groupCount = Math.max(groupCount, packedAlignments.size());
}
}
}
return groupCount;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (reader != null) {
try {
reader.close();
} catch (IOException ex) {
log.error("Error closing AlignmentQueryReader. ", ex);
}
}
}
public void updatePEStats(AlignmentTrack.RenderOptions renderOptions) {
if (this.peStats != null) {
for (PEStats stats : peStats.values()) {
stats.computeInsertSize(renderOptions.getMinInsertSizePercentile(), renderOptions.getMaxInsertSizePercentile());
}
}
}
public SpliceJunctionHelper.LoadOptions getSpliceJunctionLoadOptions() {
return loadOptions;
}
public void setMinJunctionCoverage(int minJunctionCoverage) {
this.loadOptions = new SpliceJunctionHelper.LoadOptions(minJunctionCoverage, this.loadOptions.minReadFlankingWidth);
for (AlignmentInterval interval : intervalCache.values()) {
interval.getSpliceJunctionHelper().setLoadOptions(this.loadOptions);
}
}
public void alleleThresholdChanged() {
coverageTrack.setSnpThreshold(PreferencesManager.getPreferences().getAsFloat(SAM_ALLELE_THRESHOLD));
}
public void setShowAlignments(boolean showAlignments) {
if (showAlignments != this.showAlignments) {
this.showAlignments = showAlignments;
if (showAlignments == false) {
dumpAlignments();
} else {
// Change from false => true, need to reload
intervalCache.clear();
}
}
}
public boolean isTenX() {
return reader.isTenX();
}
public boolean isPhased() {
return reader.isPhased();
}
public boolean isMoleculo() {
return reader.isMoleculo();
}
public Collection<AlignmentInterval> getLoadedIntervals() {
return intervalCache.values();
}
public static class DownsampleOptions {
private boolean downsample;
private int sampleWindowSize;
private int maxReadCount;
public DownsampleOptions() {
IGVPreferences prefs = PreferencesManager.getPreferences();
init(prefs.getAsBoolean(SAM_DOWNSAMPLE_READS),
prefs.getAsInt(SAM_SAMPLING_WINDOW),
prefs.getAsInt(SAM_SAMPLING_COUNT));
}
DownsampleOptions(boolean downsample, int sampleWindowSize, int maxReadCount) {
init(downsample, sampleWindowSize, maxReadCount);
}
private void init(boolean downsample, int sampleWindowSize, int maxReadCount) {
this.downsample = downsample;
this.sampleWindowSize = sampleWindowSize;
this.maxReadCount = maxReadCount;
}
public boolean isDownsample() {
return downsample;
}
public int getSampleWindowSize() {
return sampleWindowSize;
}
public int getMaxReadCount() {
return maxReadCount;
}
}
static class IntervalCache {
private int maxSize;
ArrayList<AlignmentInterval> intervals;
public IntervalCache() {
this(1);
}
public IntervalCache(int ms) {
this.maxSize = Math.max(1, ms);
intervals = new ArrayList<>(maxSize);
}
void setMaxSize(int ms, List<ReferenceFrame> frames) {
this.maxSize = Math.max(1, ms);
if (intervals.size() > maxSize) {
// Reduce size. Try to keep intervals that cover frame ranges. This involves a linear search
// of potentially (intervals.size X frames.size) elements. Don't attempt if this number is too large
if (frames.size() * intervals.size() < 25) {
ArrayList<AlignmentInterval> tmp = new ArrayList<>(maxSize);
for (AlignmentInterval interval : intervals) {
if (tmp.size() == maxSize) break;
for (ReferenceFrame frame : frames) {
Range range = frame.getCurrentRange();
if (interval.contains(range.getChr(), range.getStart(), range.getEnd())) {
tmp.add(interval);
break;
}
}
}
intervals = tmp;
} else {
intervals = new ArrayList(intervals.subList(0, maxSize));
intervals.trimToSize();
}
}
}
public void add(AlignmentInterval interval) {
if (intervals.size() >= maxSize) {
intervals.remove(0);
}
intervals.add(interval);
}
public AlignmentInterval getIntervalForRange(Range range) {
for (AlignmentInterval interval : intervals) {
if (interval.contains(range.getChr(), range.getStart(), range.getEnd())) {
return interval;
}
}
return null;
}
public Collection<AlignmentInterval> values() {
return intervals;
}
public void clear() {
intervals.clear();
}
}
}