@gmod/jbrowse
Version:
JBrowse - client-side genome browser
241 lines (209 loc) • 8.55 kB
JavaScript
define([
'dojo/_base/declare',
'dojo/_base/array',
'dojo/Deferred',
'JBrowse/Model/SimpleFeature',
'JBrowse/Store/SeqFeature/CombinationBase'
],
function(
declare,
array,
Deferred,
SimpleFeature,
CombinationBaseStore
) {
return declare([CombinationBaseStore], {
// An implementation of CombinationBase that deals with set-type features (without score, as in HTMLFeatures tracks).
// Usual operations are things like intersection, union, set subtraction and XOR.
// Creates features from spans. Essentially copies the basic span information and adds a feature id.
createFeatures: function(spans) {
var features = [];
//Validate this next time...
for(var span in spans) {
var id = "comfeat_" + spans[span].start + "." + spans[span].end + "." + spans[span].strand;
features.push(new SimpleFeature({data: {start: spans[span].start, end: spans[span].end, strand: spans[span].strand}, id: id}));
}
return features;
},
// Defines the various set-theoretic operations that may occur and assigns each to a span-making function.
// Passes the two sets of spans to the appropriate operator function.
opSpan: function(op, span1, span2, query) {
switch (op) {
case "&" :
return this.andSpan(span1, span2);
case "U" :
return this.orSpan(span1, span2);
case "X" :
return this.andSpan(this.orSpan(span1, span2), this.notSpan(this.andSpan(span1, span2), query));
case "S" :
return this.andSpan( span1, this.notSpan(span2, query) );
default :
console.error("Invalid boolean operation: "+op);
}
return undefined;
},
// given a set of features, takes the "union" of them and outputs a single set of nonoverlapping spans
toSpan: function(features, query) {
// strip away extra stuff and keep only the relevant feature data
var rawSpans = this._rawToSpan(features,query);
// Splits the spans based on which strand they're on, and remove overlap from each strand's spans, recombining at the end.
return this._removeOverlap(this._strandFilter(rawSpans, +1)).concat(this._removeOverlap(this._strandFilter(rawSpans, -1)));
},
_rawToSpan: function( features, query ) {
// given a set of features, makes a set of spans with the
// same start and end points (a.k.a. pseudo-features)
var spans = [];
for (var feature in features) {
if (features.hasOwnProperty(feature)) {
spans.push( { start: features[feature].get('start'), //Math.max( features[feature].get('start'), query.start ),
end: features[feature].get('end'), //Math.min( features[feature].get('end'), query.end ),
strand: features[feature].get('strand') } );
} }
return spans;
},
// Filters an array of spans based on which strand of the reference sequence they are attached to
_strandFilter: function( spans, strand ) {
return array.filter( spans, function(item) {
return item.strand == strand || !item.strand;
}).map( function(item) {
if(!item.strand)
return { start: item.start, end: item.end, strand: strand } // Adds strand to strandless spans
else
return item;
});
},
// converts overlapping spans into their union. Assumes the spans are all on the same strand.
_removeOverlap: function( spans ) {
if(!spans.length) {
return [];
}
spans.sort(function(a,b) { return a.start - b.start; });
return this._removeOverlapSorted(spans);
},
// Given an array of spans sorted by their start bp, converts them into a single non-overlapping set (ie takes their union).
_removeOverlapSorted: function( spans ) {
var retSpans = [];
var i = 0;
var strand = spans[0].strand;
while(i < spans.length) {
var start = spans[i].start;
var end = spans[i].end;
while(i < spans.length && spans[i].start <= end) {
end = Math.max(end, spans[i].end);
i++;
}
retSpans.push( { start: start, end: end, strand: strand});
}
return retSpans;
},
// given two sets of spans without internal overlap, outputs a set corresponding to their union.
orSpan: function( span1, span2 ){
return this._computeUnion(this._strandFilter(span1, 1), this._strandFilter(span2, 1))
.concat(this._computeUnion(this._strandFilter(span1,-1), this._strandFilter(span2,-1)));
},
// given two sets of spans without internal overlap, outputs a set corresponding to their intersection
andSpan: function( span1, span2){
return this._computeIntersection(this._strandFilter(span1, 1), this._strandFilter(span2,1))
.concat(this._computeIntersection(this._strandFilter(span1,-1), this._strandFilter(span2,-1)));
},
// This method should merge two sorted span arrays in O(n) time, which is better
// then using span1.concat(span2) and then array.sort(), which takes O(n*log(n)) time.
_sortedArrayMerge: function( span1, span2) {
var newArray = [];
var i = 0;
var j = 0;
while(i < span1.length && j < span2.length) {
if( span1[i].start <= span2[j].start ) {
newArray.push(span1[i]);
i++;
} else {
newArray.push(span2[j]);
j++;
}
}
if(i < span1.length) {
newArray = newArray.concat(span1.slice(i, span1.length));
} else if(j < span2.length) {
newArray = newArray.concat(span2.slice(j, span2.length));
}
return newArray;
},
// A helper method for computing the union of two arrays of spans.
_computeUnion: function( span1, span2) {
if(!span1.length && !span2.length) {
return [];
}
return this._removeOverlapSorted(this._sortedArrayMerge(span1,span2));
},
// A helper method for computing the intersection of two arrays of spans.
_computeIntersection: function( span1, span2) {
if(!span1.length || !span2.length) {
return [];
}
var allSpans = this._sortedArrayMerge(span1, span2);
var retSpans = [];
var maxEnd = allSpans[0].end;
var strand = span1[0].strand; // Assumes both span sets contain only features for one specific strand
var i = 1;
while(i < allSpans.length) {
var start = allSpans[i].start;
var end = Math.min(allSpans[i].end, maxEnd);
if(start < end) {
retSpans.push({start: start, end: end, strand: strand});
}
maxEnd = Math.max(allSpans[i].end, maxEnd);
i++;
}
return retSpans;
},
// Filters span set by strand, inverts the sets represented on each strand, and recombines.
notSpan: function( spans, query) {
return this._rawNotSpan(this._strandFilter(spans, +1), query, +1).concat(this._rawNotSpan(this._strandFilter(spans, -1), query, -1));
},
// Converts a set of spans into its complement in the reference sequence.
_rawNotSpan: function( spans, query, strand ) {
var invSpan = [];
invSpan[0] = { start: query.start };
var i = 0;
for (var span in spans) {
if (spans.hasOwnProperty(span)) {
span = spans[span];
invSpan[i].strand = strand;
invSpan[i].end = span.start;
i++;
invSpan[i] = { start: span.end };
}
}
invSpan[i].strand = strand;
invSpan[i].end = query.end;
if (invSpan[i].end <= invSpan[i].start) {
invSpan.splice(i,1);
}
if (invSpan[0].end <= invSpan[0].start) {
invSpan.splice(0,1);
}
return invSpan;
},
loadRegion: function ( region ) {
var d = new Deferred();
if(this.stores.length == 1) {
d.resolve(this, true);
return d.promise;
}
var thisB = this;
var regionLoaded = region;
regionLoaded.spans = [];
delete this.regionLoaded;
this._getFeatures(region, function(){}, function(results){
if(results && results.spans) {
regionLoaded.spans = results.spans;
thisB.regionLoaded = regionLoaded;
}
d.resolve(thisB, true);
}, function(){
d.reject("cannot load region");
});
return d.promise;
}
});
});