/*
* Copyright (c) 2007-2013 The Broad Institute, Inc.
* SOFTWARE COPYRIGHT NOTICE
* This software and its documentation are the copyright of the Broad Institute, Inc. All rights are reserved.
*
* This software is supplied without any warranty or guaranteed support whatsoever. The Broad Institute is not responsible for its use, misuse, or functionality.
*
* This software is licensed under the terms of the GNU Lesser General Public License (LGPL),
* Version 2.1 which is available at http://www.opensource.org/licenses/lgpl-2.1.php.
*/
package org.broad.igv.data;
import com.google.common.collect.Iterators;
import org.apache.log4j.Logger;
import org.broad.igv.feature.LocusScore;
import org.broad.igv.session.IGVSessionReader;
import org.broad.igv.session.SessionXmlAdapters;
import org.broad.igv.session.SubtlyImportant;
import org.broad.igv.track.DataTrack;
import org.broad.igv.track.Track;
import org.broad.igv.track.TrackType;
import org.broad.igv.track.WindowFunction;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.util.*;
/**
* Data source which combines two other DataSources
*
* TODO Multiple DataSources. There is no 2
* @author jrobinso, jacob
*/
@XmlAccessorType(XmlAccessType.NONE)
public class CombinedDataSource implements DataSource {
private static Logger log = Logger.getLogger(CombinedDataSource.class);
public enum Operation{
ADD("+"),
SUBTRACT("-"),
MULTIPLY("*"),
DIVIDE("/");
private String stringRep;
private Operation(String stringrep){
this.stringRep = stringrep;
}
}
@XmlJavaTypeAdapter(SessionXmlAdapters.DataTrackIDAdapter.class)
@XmlAttribute(name = "source0")
DataTrack source0;
@XmlJavaTypeAdapter(SessionXmlAdapters.DataTrackIDAdapter.class)
@XmlAttribute(name = "source1")
DataTrack source1;
@XmlAttribute
Operation operation = Operation.ADD;
@SubtlyImportant
private CombinedDataSource(){}
public CombinedDataSource(DataTrack source0, DataTrack source1, Operation operation){
this.source0 = source0;
this.source1 = source1;
this.operation = operation;
}
public void updateTrackReferences(List<Track> allTracks) {
//We filled in sources with placeholder tracks if not found, now find the real ones
source0 = updateTrackReference(source0, allTracks);
source1 = updateTrackReference(source1, allTracks);
}
private DataTrack updateTrackReference(DataTrack memberTrack, List<Track> allTracks){
if(memberTrack.getName() == null && memberTrack.getResourceLocator() == null){
DataTrack matchingTrack = (DataTrack) IGVSessionReader.getMatchingTrack(memberTrack.getId(), allTracks);
if(matchingTrack == null) throw new IllegalStateException("Could not find track with ID " + memberTrack.getId());
return matchingTrack;
}else{
return memberTrack;
}
}
public List<LocusScore> getSummaryScoresForRange(String chr, int startLocation, int endLocation, int zoom){
List<LocusScore> outerScores = this.source0.getSummaryScores(chr, startLocation, endLocation, zoom);
List<LocusScore> innerScores = this.source1.getSummaryScores(chr, startLocation, endLocation, zoom);
int initialSize = outerScores.size() + innerScores.size();
List<LocusScore> combinedScoresList = new ArrayList<LocusScore>(initialSize);
if(initialSize == 0) return combinedScoresList;
//TODO We assume that having no data from one source is the identity operation, that may not be true
if(innerScores.size() == 0) return outerScores;
if(outerScores.size() == 0) return innerScores;
/**
* We first generate the chunks which will need to be calculated separately
* This is the set of all start/end positions of outerScores and innerScores
* We could be a bit smarter, but this is simpler and there's no problem with
* skipping over intervals which don't have data later.
*
* Following that, for each interval generated, we search outerScores and innerScores
* for the unique LocusScore which contains the generated interval.
*/
//Generate the boundaries for the new combined regions
Set<Integer> boundariesSet = new LinkedHashSet<Integer>(2*initialSize);
Iterator<LocusScore> dualIter = Iterators.mergeSorted(Arrays.asList(innerScores.iterator(), outerScores.iterator()),
new Comparator<LocusScore>() {
@Override
public int compare(LocusScore o1, LocusScore o2) {
return o1.getStart() - o2.getStart();
}
});
while(dualIter.hasNext()){
LocusScore score = dualIter.next();
boundariesSet.add(score.getStart());
boundariesSet.add(score.getEnd());
}
Integer[] boundariesArray = boundariesSet.toArray(new Integer[0]);
Arrays.sort(boundariesArray);
int outerScoreInd = 0;
int innerScoreInd = 0;
//Calculate value for each interval
for(int bb=0; bb < boundariesArray.length-1; bb++){
int start = boundariesArray[bb];
int end = boundariesArray[bb+1];
//It shouldn't be possible for more than one LocusScore of either
//tracks to overlap each interval, since the start/ends
//were based on all start/ends of the inputs
outerScoreInd = findContains(start, end, outerScores, Math.max(outerScoreInd, 0));
innerScoreInd = findContains(start, end, innerScores, Math.max(innerScoreInd, 0));
LocusScore outerScore = getContains(outerScores, outerScoreInd);
LocusScore innerScore = getContains(innerScores, innerScoreInd);
if(outerScore == null && innerScore == null) continue;
float score = combineScores(outerScore, innerScore);
BasicScore newScore = new BasicScore(start, end, score);
combinedScoresList.add(newScore);
}
return combinedScoresList;
}
/**
* Search {@code scoresList} (must be sorted by start position) for a score which contains the interval specified
* by start/end. The first one which satisfies this requirement is returned.
*
* @param start
* @param end
* @param scoresList
* @param startIndex Optimization, where to start searching in {@code scoresList}
**/
private int findContains(int start, int end, List<LocusScore> scoresList, int startIndex) {
for(int ii=startIndex; ii < scoresList.size(); ii++){
LocusScore score = scoresList.get(ii);
if(score.getStart() <= start && score.getEnd() >= end){
return ii;
}else if(score.getStart() >= end){
return -1;
}
}
return -1;
}
private LocusScore getContains(List<LocusScore> scores, int index){
if(index >= 0 && index < scores.size()){
return scores.get(index);
}else{
return null;
}
}
/**
* Combine the scores using this sources operation. Either can be null,
* in which case it is given the coordinates of the other and a score of 0.
* Both inputs cannot be null
* @param score0
* @param score1
* @return
*/
private float combineScores(LocusScore score0, LocusScore score1) {
if(score0 == null && score1 == null) throw new IllegalArgumentException("Both inputs cannot be null");
if(score0 == null){
score0 = new BasicScore(score1.getStart(), score1.getEnd(), 0.0f);
}else if(score1 == null){
score1 = new BasicScore(score0.getStart(), score0.getEnd(), 0.0f);
}
switch(operation){
case ADD:
return score0.getScore() + score1.getScore();
case SUBTRACT:
return score0.getScore() - score1.getScore();
case MULTIPLY:
return score0.getScore() * score1.getScore();
case DIVIDE:
if(score1.getScore() == 0.0f){
return 0.0f;
}
return score0.getScore() / score1.getScore();
default:
throw new IllegalStateException("Operation not recognized: " + operation);
}
}
public double getDataMax() {
return 0;
}
public double getDataMin() {
return 0;
}
public TrackType getTrackType() {
return TrackType.PLUGIN;
}
public void setWindowFunction(WindowFunction statType) {
//TODO
}
public boolean isLogNormalized() {
return false;
}
public WindowFunction getWindowFunction() {
return WindowFunction.none;
}
public Collection<WindowFunction> getAvailableWindowFunctions() {
return new ArrayList<WindowFunction>();
}
@Override
public void dispose() {
if(source0 != null) source0.dispose();
if(source1 != null) source1.dispose();
}
}