From 6bbd62f031698aee6faad53c650878793af39911 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Fri, 8 Dec 2017 17:15:39 -0800 Subject: [PATCH 01/21] parsing forward okay, but is reading lines backwards for some reason --- client/apollo/js/Store/SeqFeature/GFF3.js | 9 +- .../apollo/js/Store/SeqFeature/GFF3/Parser.js | 39 ++++- client/apollo/js/Util/GFF3.js | 159 ++++++++++++++++++ 3 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 client/apollo/js/Util/GFF3.js diff --git a/client/apollo/js/Store/SeqFeature/GFF3.js b/client/apollo/js/Store/SeqFeature/GFF3.js index 7cdfbc3a09..62562541f1 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3.js +++ b/client/apollo/js/Store/SeqFeature/GFF3.js @@ -52,8 +52,8 @@ return declare([ GFF3 ], var features = []; this._getFeatures({ ref: sequenceListObject[0].name, - start: unprojectedStart, - end: unprojectedEnd + start: start, + end:end }, function( feature ) { features.push(feature); @@ -142,13 +142,15 @@ return declare([ GFF3 ], thisB._deferred.features.resolve( features ); console.log('Parse final: ' +features.length); - console.log(features) } }); var fail = lang.hitch( this, '_failAllDeferred' ); // parse the whole file and store it this.data.fetchLines( function( line ) { + // console.log(line) ; + // line = line.split("").reverse().join("").replace(" ","\t").slice(1,-1); + // console.log(line) ; try { parser.addLine(line); } catch(e) { @@ -164,7 +166,6 @@ return declare([ GFF3 ], _getFeatures: function( query, featureCallback, finishedCallback, errorCallback ) { var thisB = this; thisB._deferred.features.then( function() { - console.log('got features? ' +thisB._deferred.features); thisB._search( query, featureCallback, finishedCallback, errorCallback ); }); }, diff --git a/client/apollo/js/Store/SeqFeature/GFF3/Parser.js b/client/apollo/js/Store/SeqFeature/GFF3/Parser.js index f144b6a86f..b8061d8399 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3/Parser.js +++ b/client/apollo/js/Store/SeqFeature/GFF3/Parser.js @@ -3,7 +3,7 @@ define([ 'dojo/_base/array', 'dojo/_base/lang', 'dojo/json', - 'JBrowse/Util/GFF3', + 'WebApollo/Util/GFF3', 'JBrowse/Store/SeqFeature/GFF3/Parser' ], function( @@ -17,6 +17,36 @@ define([ return declare( [Parser], { + constructor: function( args ) { + lang.mixin( this, { + featureCallback: args.featureCallback || function() {}, + endCallback: args.endCallback || function() {}, + commentCallback: args.commentCallback || function() {}, + errorCallback: args.errorCallback || function(e) { console.error(e); }, + directiveCallback: args.directiveCallback || function() {}, + + // features that we have to keep on hand for now because they + // might be referenced by something else + under_construction_top_level : [], + // index of the above by ID + under_construction_by_id : {}, + + completed_references: {}, + + // features that reference something we have not seen yet + // structured as: + // { 'some_id' : { + // 'Parent' : [ orphans that have a Parent attr referencing it ], + // 'Derives_from' : [ orphans that have a Derives_from attr referencing it ], + // } + under_construction_orphans : {}, + + // if this is true, the parser ignores the + // rest of the lines in the file. currently + // set when the file switches over to FASTA + eof: false + }); + }, addLine: function( line ) { var match; @@ -84,8 +114,8 @@ return declare( [Parser], { // much larger than the item_buffer, we swap them and unshift the // existing buffer onto it to avoid a big copy. array.forEach( this.under_construction_top_level, - this._return_item, - this ); + this._return_item, + this ); this.under_construction_top_level = []; this.under_construction_by_id = {}; @@ -139,7 +169,7 @@ return declare( [Parser], { this._resolve_references_to( feature, id ); } },this); - + // try to resolve all its references this._resolve_references_from( feature || [ feature_line ], { Parent : parents, Derives_from : derives }, ids ); }, @@ -184,5 +214,6 @@ return declare( [Parser], { },this); } } + }); }); diff --git a/client/apollo/js/Util/GFF3.js b/client/apollo/js/Util/GFF3.js new file mode 100644 index 0000000000..38ae7bf299 --- /dev/null +++ b/client/apollo/js/Util/GFF3.js @@ -0,0 +1,159 @@ +/** + * Fast, low-level functions for parsing and formatting GFF3. + * JavaScript port of Robert Buels's Bio::GFF3::LowLevel Perl module. + */ + +define([ + 'dojo/_base/array', + 'dojo/_base/lang' + ], + function( + array, + lang + ) { +var gff3_field_names = 'seq_id source type start end score strand phase attributes'.split(' '); + +return { + + parse_feature: function( line ) { + var f = array.map( line.split("\t"), function(a) { + if( a == '.' ) { + return null; + } + return a; + }); + + // unescape only the ref and source columns + f[0] = this.unescape( f[0] ); + f[1] = this.unescape( f[1] ); + + f[8] = this.parse_attributes( f[8] ); + var parsed = {}; + for( var i = 0; i < gff3_field_names.length; i++ ) { + parsed[ gff3_field_names[i] ] = f[i] == '.' ? null : f[i]; + } + + if( parsed.start !== null ) + parsed.start = parseInt( parsed.start, 10 ); + if( parsed.end !== null ) + parsed.end = parseInt( parsed.end, 10 ); + if( parsed.score !== null ) + parsed.score = parseFloat( parsed.score, 10 ); + if( parsed.strand != null ) + parsed.strand = {'+':1,'-':-1}[parsed.strand] || 0; + return parsed; + }, + + parse_directive: function( line ) { + var match = /^\s*\#\#\s*(\S+)\s*(.*)/.exec( line ); + if( ! match ) + return null; + var name = match[1], contents = match[2]; + + var parsed = { directive : name }; + if( contents.length ) { + contents = contents.replace( /\r?\n$/, '' ); + parsed.value = contents; + } + + // do a little additional parsing for sequence-region and genome-build directives + if( name == 'sequence-region' ) { + var c = contents.split( /\s+/, 3 ); + parsed.seq_id = c[0]; + parsed.start = c[1].replace(/\D/g,''); + parsed.end = c[2].replace(/\D/g,''); + } + else if( name == 'genome-build' ) { + var c = contents.split( /\s+/, 2 ); + parsed.source = c[0]; + parsed.buildname = c[1]; + } + + return parsed; + }, + + unescape: function( s ) { + if( s === null ) + return null; + + return s.replace( /%([0-9A-Fa-f]{2})/g, function( match, seq ) { + return String.fromCharCode( parseInt( seq, 16 ) ); + }); + }, + + escape: function( s ) { + return s.replace( /[\n\r\t;=%&,\x00-\x1f\x7f-\xff]/g, function( ch ) { + var hex = ch.charCodeAt(0).toString(16).toUpperCase(); + if( hex.length < 2 ) // lol, apparently there's no native function for fixed-width hex output + hex = '0'+hex; + return '%'+hex; + }); + }, + + parse_attributes: function( attrString ) { + + if( !( attrString && attrString.length ) || attrString == '.' ) + return {}; + + attrString = attrString.replace(/\r?\n$/, '' ); + + var attrs = {}; + array.forEach( attrString.split(';'), function( a ) { + var nv = a.split( '=', 2 ); + if( !( nv[1] && nv[1].length ) ) + return; + var arec = attrs[ nv[0] ]; + if( ! arec ) + arec = attrs[ nv[0] ] = []; + + arec.push.apply( + arec, + array.map( + nv[1].split(','), + this.unescape + )); + },this); + + return attrs; + }, + + format_feature: function( f ) { + var attrString = + f.attributes === null || typeof f.attributes == 'undefined' + ? '.' : this.format_attributes( f.attributes ); + + var translate_strand=['-','.','+']; + var fields = []; + for( var i = 0; i<8; i++ ) { + var val = f[ gff3_field_names[i] ]; + if(i==6) // deserialize strand + fields[i] = val === null || val === undefined ? '.' : translate_strand[val+1]; + else + fields[i] = val === null || val === undefined ? '.' : this.escape( ''+val ); + } + fields[8] = attrString; + + return fields.join("\t")+"\n"; + }, + + format_attributes: function( attrs ) { + var attrOrder = []; + for( var tag in attrs ) { + var val = attrs[tag]; + var valstring = val.hasOwnProperty( 'toString' ) + ? this.escape( val.toString() ) : + lang.isArray(val.values) + ? function(val) { + return lang.isArray(val) + ? array.map( val, this.escape ).join(',') + : this.escape( val ); + }.call(this,val.values) : + lang.isArray(val) + ? array.map( val, this.escape ).join(',') + : this.escape( val ); + attrOrder.push( this.escape( tag )+'='+valstring); + } + return attrOrder.length ? attrOrder.join(';') : '.'; + } +}; +}); From 2b7234f4c3dc1af283d48e76f39111c28490b93e Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Mon, 11 Dec 2017 17:27:28 -0800 Subject: [PATCH 02/21] a bit further, when reversing --- client/apollo/js/Model/FileBlob.js | 102 +++++++++ client/apollo/js/Model/XHRBlob.js | 105 +++++++++ client/apollo/js/ProjectionUtils.js | 19 +- client/apollo/js/Store/SeqFeature/GFF3.js | 200 ++++++++++-------- .../SeqFeature/GlobalStatsEstimationMixin.js | 2 +- 5 files changed, 342 insertions(+), 86 deletions(-) create mode 100644 client/apollo/js/Model/FileBlob.js create mode 100644 client/apollo/js/Model/XHRBlob.js diff --git a/client/apollo/js/Model/FileBlob.js b/client/apollo/js/Model/FileBlob.js new file mode 100644 index 0000000000..1a3159fbcd --- /dev/null +++ b/client/apollo/js/Model/FileBlob.js @@ -0,0 +1,102 @@ +define([ + 'dojo/_base/declare', + 'dojo/_base/array', + 'dojo/has', + 'JBrowse/Util/TextIterator' + ], + function( declare, array, has, TextIterator ) { +var FileBlob = declare( null, +/** + * @lends JBrowse.Model.FileBlob.prototype + */ +{ + + /** + * Blob of binary data fetched from a local file (with FileReader). + * + * Adapted by Robert Buels from the BlobFetchable object in the + * Dalliance Genome Explorer, which was is copyright Thomas Down + * 2006-2011. + * @constructs + */ + constructor: function(b) { + this.blob = b; + this.size = b.size; + this.totalSize = b.size; + }, + + slice: function(start, length) { + var sliceFunc = this.blob.mozSlice || this.blob.slice || this.blob.webkitSlice; + return new FileBlob( + length ? sliceFunc.call( this.blob, start, start + length ) + : sliceFunc.call( this.blob, start ) + ); + }, + + fetchLines: function( lineCallback, endCallback, failCallback ) { + var thisB = this; + this.fetch( function( data ) { + data = new Uint8Array(data); + + var lineIterator = new TextIterator.FromBytes( + { bytes: data, + // only return a partial line at the end + // if we are not operating on a slice of + // the file + returnPartialRecord: !this.end + }); + var line; + while(( line = lineIterator.getline() )) { + lineCallback( line ); + } + + endCallback(); + }, failCallback ); + }, + + readLines: function( offset, length, lineCallback, endCallback, failCallback ) { + var start = this.start + offset, + end = start + length; + var skipFirst = offset != 0; + this.slice( offset, length ) + .fetchLines( + function() { + // skip the first line if we have a + // nonzero offset, because it is probably + // incomplete + if( ! skipFirst ) + lineCallback(); + skipFirst = false; + }, endCallback, failCallback ); + }, + + read: function( offset, length, callback, failCallback ) { + var start = this.start + offset, + end = start + length; + this.slice( offset, length ) + .fetch( callback, failCallback ); + }, + + fetch: function( callback, failCallback ) { + var that = this, + reader = new FileReader(); + reader.onloadend = function(ev) { + callback( that._stringToBuffer( reader.result ) ); + }; + reader.readAsBinaryString( this.blob ); + }, + + _stringToBuffer: function(result) { + if( ! result || ! has('typed-arrays') ) + return null; + + var ba = new Uint8Array( result.length ); + for ( var i = 0; i < ba.length; i++ ) { + ba[i] = result.charCodeAt(i); + } + return ba.buffer; + } + +}); +return FileBlob; +}); \ No newline at end of file diff --git a/client/apollo/js/Model/XHRBlob.js b/client/apollo/js/Model/XHRBlob.js new file mode 100644 index 0000000000..61d5444632 --- /dev/null +++ b/client/apollo/js/Model/XHRBlob.js @@ -0,0 +1,105 @@ +define( [ 'dojo/_base/declare', + 'JBrowse/Model/XHRBlob', + 'JBrowse/Store/RemoteBinaryFile' + ], + function( declare, XHR ,RemoteBinaryFileCache ) { +var globalCache = new RemoteBinaryFileCache({ + name: 'XHRBlob', + maxSize: 100000000 // 100MB of file cache +}); + +var XHRBlob = declare( XHR, +/** + * @lends JBrowse.Model.XHRBlob.prototype + */ +{ + + /** + * Blob of binary data fetched with an XMLHTTPRequest. + * + * Adapted by Robert Buels from the URLFetchable object in the + * Dalliance Genome Explorer, which was is copyright Thomas Down + * 2006-2011. + * @constructs + */ + constructor: function(url, start, end, opts) { + if (!opts) { + if (typeof start === 'object') { + opts = start; + start = undefined; + } else { + opts = {}; + } + } + + this.url = url; + this.start = start || 0; + if (end) { + this.end = end; + } + this.opts = opts; + }, + + slice: function(s, l) { + var ns = this.start, ne = this.end; + if (ns && s) { + ns = ns + s; + } else { + ns = s || ns; + } + if (l && ns) { + ne = ns + l - 1; + } else { + ne = ne || l - 1; + } + return new XHRBlob(this.url, ns, ne, this.opts); + }, + + reverseFetchLines: function( lineCallback, endCallback, failCallback ) { + var thisB = this; + this.fetch( function( data ) { + data = new Uint8Array(data); + data = data.reverse(); + + var lineIterator = new TextIterator.FromBytes( + { bytes: data, + // only return a partial line at the end + // if we are not operating on a slice of + // the file + returnPartialRecord: !this.end + }); + var line; + while(( line = lineIterator.getline() )) { + lineCallback( line ); + } + + endCallback(); + }, failCallback ); + }, + + + fetch: function( callback, failCallback ) { + globalCache.get({ + url: this.url, + start: this.start, + end: this.end, + success: callback, + failure: failCallback + }); + }, + + read: function( offset, length, callback, failCallback ) { + var start = this.start + offset, + end = start + length; + + globalCache.get({ + url: this.url, + start: start, + end: end, + success: callback, + failure: failCallback + }); + } +}); +return XHRBlob; +}); \ No newline at end of file diff --git a/client/apollo/js/ProjectionUtils.js b/client/apollo/js/ProjectionUtils.js index 170ae89c7a..b2ee9629e8 100644 --- a/client/apollo/js/ProjectionUtils.js +++ b/client/apollo/js/ProjectionUtils.js @@ -65,7 +65,24 @@ define([ 'dojo/_base/declare', else{ return input ; } - } + }; + + ProjectionUtils.flipStrand = function(input){ + if(input==='+') return '-'; + if(input==='-') return '+'; + return input ; + }; + + ProjectionUtils.unProjectGFF3 = function(refSeqName,line){ + var returnArray ; + returnArray = line.split("\t"); + // var sequenceListObject = this.parseSequenceList(refSeqName); + var coords = this.unProjectCoordinates(refSeqName,returnArray[3],returnArray[4]); + returnArray[3] = coords[0]; + returnArray[4] = coords[1]; + returnArray[6] = this.flipStrand(returnArray[6]); + return returnArray.join("\t") + }; /** * Unproject the given start and end diff --git a/client/apollo/js/Store/SeqFeature/GFF3.js b/client/apollo/js/Store/SeqFeature/GFF3.js index 62562541f1..c8379bad1f 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3.js +++ b/client/apollo/js/Store/SeqFeature/GFF3.js @@ -6,7 +6,9 @@ define( [ 'WebApollo/ProjectionUtils', 'WebApollo/Store/SeqFeature/GlobalStatsEstimationMixin', 'JBrowse/Store/SeqFeature/GFF3', - 'WebApollo/Store/SeqFeature/GFF3/Parser', + 'JBrowse/Store/SeqFeature/GFF3/Parser', + 'WebApollo/Model/XHRBlob', + 'JBrowse/Util/GFF3', 'JBrowse/Errors' ], function( @@ -18,6 +20,8 @@ define( [ GlobalStatsEstimationMixin, GFF3, Parser, + XHRBlob, + GFF3Parser, Errors ) { @@ -28,82 +32,98 @@ return declare([ GFF3 ], */ { - _estimateGlobalStats: function(refseq) { - var deferred = new Deferred(); - refseq = refseq || this.refSeq; - var sequenceListObject = ProjectionUtils.parseSequenceList(refseq.name); - var timeout = this.storeTimeout || 3000; - var startTime = new Date(); - - var statsFromInterval = function( length, callback ) { - var thisB = this; - var sampleCenter; - if (sequenceListObject[0].reverse) { - sampleCenter = refseq.end * 0.75 + refseq.start * 0.25; - } - else { - sampleCenter = refseq.start * 0.75 + refseq.end * 0.25; - } - var start = Math.max( 0, Math.round( sampleCenter - length/2 ) ); - var end = Math.min( Math.round( sampleCenter + length/2 ), refseq.end ); - var unprojectedArray = ProjectionUtils.unProjectCoordinates(refseq.name, start, end); - var unprojectedStart = unprojectedArray[0]; - var unprojectedEnd = unprojectedArray[1]; - var features = []; - this._getFeatures({ - ref: sequenceListObject[0].name, - start: start, - end:end - }, - function( feature ) { - features.push(feature); - }, - function( error ) { - features = array.filter( - features, - function(f) { - return f.get('start') >= unprojectedStart && f.get('end') <= unprojectedEnd; - } - ); - callback.call( thisB, length, - { - featureDensity: features.length / length, - _statsSampleFeatures: features.length, - _statsSampleInterval: { ref: refseq.name, start: start, end: end, length: length } - }); - }, - function( error ) { - callback.call( thisB, length, null, error ); - }); - }; - - var maybeRecordStats = function( interval, stats, error ) { - if( error ) { - if( error.isInstanceOf(Errors.DataOverflow) ) { - console.log( 'Store statistics found chunkSizeLimit error, using empty: '+(this.source||this.name) ); - deferred.resolve( { featureDensity: 0, error: 'global stats estimation found chunkSizeError' } ); - } - else { - deferred.reject( error ); - } - } else { - var refLen = refseq.end - refseq.start; - if( stats._statsSampleFeatures >= 300 || interval * 2 > refLen || error ) { - console.log( 'WA Store statistics: '+(this.source||this.name), stats ); - deferred.resolve( stats ); - } else if( ((new Date()) - startTime) < timeout ) { - statsFromInterval.call( this, interval * 2, maybeRecordStats ); - } else { - console.log( 'Store statistics timed out: '+(this.source||this.name) ); - deferred.resolve( { featureDensity: 0, error: 'global stats estimation timed out' } ); - } - } - }; - - statsFromInterval.call( this, 100, maybeRecordStats ); - return deferred; + constructor: function( args ) { + console.log('over-ridden constructor with'); + console.log(args); + // this.data = args.blob || + // new XHRBlob( this.resolveUrl( + // this._evalConf(args.urlTemplate) + // ) + // ); + this.data = new XHRBlob( this.resolveUrl( + this._evalConf(args.urlTemplate) + ) + ); + this.features = []; + this._loadFeatures(); }, + // _estimateGlobalStats: function(refseq) { + // var deferred = new Deferred(); + // refseq = refseq || this.refSeq; + // var sequenceListObject = ProjectionUtils.parseSequenceList(refseq.name); + // var timeout = this.storeTimeout || 3000; + // var startTime = new Date(); + // + // var statsFromInterval = function( length, callback ) { + // var thisB = this; + // var sampleCenter; + // if (sequenceListObject[0].reverse) { + // sampleCenter = refseq.end * 0.75 + refseq.start * 0.25; + // } + // else { + // sampleCenter = refseq.start * 0.75 + refseq.end * 0.25; + // } + // var start = Math.max( 0, Math.round( sampleCenter - length/2 ) ); + // var end = Math.min( Math.round( sampleCenter + length/2 ), refseq.end ); + // var unprojectedArray = ProjectionUtils.unProjectCoordinates(refseq.name, start, end); + // var unprojectedStart = unprojectedArray[0]; + // var unprojectedEnd = unprojectedArray[1]; + // var features = []; + // this._getFeatures({ + // ref: sequenceListObject[0].name, + // start: unprojectedStart, + // end:unprojectedEnd + // }, + // function( feature ) { + // features.push(feature); + // }, + // function( error ) { + // features = array.filter( + // features, + // function(f) { + // return f.get('start') >= unprojectedStart && f.get('end') <= unprojectedEnd; + // } + // ); + // callback.call( thisB, length, + // { + // featureDensity: features.length / length, + // _statsSampleFeatures: features.length, + // _statsSampleInterval: { ref: refseq.name, start: unprojectedStart, end: unprojectedEnd, length: length } + // }); + // }, + // function( error ) { + // callback.call( thisB, length, null, error ); + // }); + // }; + // + // var maybeRecordStats = function( interval, stats, error ) { + // if( error ) { + // if( error.isInstanceOf(Errors.DataOverflow) ) { + // console.log( 'Store statistics found chunkSizeLimit error, using empty: '+(this.source||this.name) ); + // deferred.resolve( { featureDensity: 0, error: 'global stats estimation found chunkSizeError' } ); + // } + // else { + // deferred.reject( error ); + // } + // } else { + // var refLen = refseq.end - refseq.start; + // if( stats._statsSampleFeatures >= 300 || interval * 2 > refLen || error ) { + // console.log( 'WA Store statistics: '+(this.source||this.name), stats ); + // deferred.resolve( stats ); + // } else if( ((new Date()) - startTime) < timeout ) { + // statsFromInterval.call( this, interval * 2, maybeRecordStats ); + // } else { + // console.log( 'Store statistics timed out: '+(this.source||this.name) ); + // deferred.resolve( { featureDensity: 0, error: 'global stats estimation timed out' } ); + // } + // } + // }; + // + // statsFromInterval.call( this, 100, maybeRecordStats ); + // return deferred; + // }, + _loadFeatures: function() { var thisB = this; var features = this.bareFeatures = []; @@ -146,11 +166,23 @@ return declare([ GFF3 ], }); var fail = lang.hitch( this, '_failAllDeferred' ); // parse the whole file and store it + var sequenceListObject = ProjectionUtils.parseSequenceList(thisB.refSeq.name); + var reverse = sequenceListObject[0].reverse ; + var lines = []; + // this.data.reverseFetchLines( this.data.fetchLines( function( line ) { - // console.log(line) ; - // line = line.split("").reverse().join("").replace(" ","\t").slice(1,-1); - // console.log(line) ; + if(reverse && line.length>30) { + // console.log('reversing ->['+line+']'); + line = line.split("").reverse().join("").replace(" ", "\t").slice(0, -1).replace('\n', ''); + } + if(line.length > 30){ + // console.log('unprojecting ->['+line+']'); + line = ProjectionUtils.unProjectGFF3(thisB.refSeq.name,line); + // console.log('unprojectED ->['+line+']'); + } + console.log(line) ; + lines.push(line); try { parser.addLine(line); } catch(e) { @@ -163,12 +195,12 @@ return declare([ GFF3 ], ); }, - _getFeatures: function( query, featureCallback, finishedCallback, errorCallback ) { - var thisB = this; - thisB._deferred.features.then( function() { - thisB._search( query, featureCallback, finishedCallback, errorCallback ); - }); - }, + // _getFeatures: function( query, featureCallback, finishedCallback, errorCallback ) { + // var thisB = this; + // thisB._deferred.features.then( function() { + // thisB._search( query, featureCallback, finishedCallback, errorCallback ); + // }); + // }, _search: function( query, featureCallback, finishCallback, errorCallback ) { // search in this.features, which are sorted diff --git a/client/apollo/js/Store/SeqFeature/GlobalStatsEstimationMixin.js b/client/apollo/js/Store/SeqFeature/GlobalStatsEstimationMixin.js index acbd8f3f43..0f9b38127d 100644 --- a/client/apollo/js/Store/SeqFeature/GlobalStatsEstimationMixin.js +++ b/client/apollo/js/Store/SeqFeature/GlobalStatsEstimationMixin.js @@ -8,7 +8,7 @@ define([ 'dojo/_base/declare', 'dojo/_base/array', 'dojo/Deferred', - 'WebApollo/ProjectionUtils', + 'WebApollo/ProjectionUtils', 'JBrowse/Errors' ], function( declare, array, Deferred, ProjectionUtils, Errors ) { From 4eb6e3b44978b2621dbaee451dd7fc805572d81d Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Mon, 11 Dec 2017 19:52:34 -0800 Subject: [PATCH 03/21] got HTMLFeatures to project correctly --- client/apollo/js/Model/FileBlob.js | 102 ----------------- client/apollo/js/Model/XHRBlob.js | 105 ----------------- client/apollo/js/ProjectionUtils.js | 6 +- client/apollo/js/Store/SeqFeature/GFF3.js | 132 ++-------------------- 4 files changed, 12 insertions(+), 333 deletions(-) delete mode 100644 client/apollo/js/Model/FileBlob.js delete mode 100644 client/apollo/js/Model/XHRBlob.js diff --git a/client/apollo/js/Model/FileBlob.js b/client/apollo/js/Model/FileBlob.js deleted file mode 100644 index 1a3159fbcd..0000000000 --- a/client/apollo/js/Model/FileBlob.js +++ /dev/null @@ -1,102 +0,0 @@ -define([ - 'dojo/_base/declare', - 'dojo/_base/array', - 'dojo/has', - 'JBrowse/Util/TextIterator' - ], - function( declare, array, has, TextIterator ) { -var FileBlob = declare( null, -/** - * @lends JBrowse.Model.FileBlob.prototype - */ -{ - - /** - * Blob of binary data fetched from a local file (with FileReader). - * - * Adapted by Robert Buels from the BlobFetchable object in the - * Dalliance Genome Explorer, which was is copyright Thomas Down - * 2006-2011. - * @constructs - */ - constructor: function(b) { - this.blob = b; - this.size = b.size; - this.totalSize = b.size; - }, - - slice: function(start, length) { - var sliceFunc = this.blob.mozSlice || this.blob.slice || this.blob.webkitSlice; - return new FileBlob( - length ? sliceFunc.call( this.blob, start, start + length ) - : sliceFunc.call( this.blob, start ) - ); - }, - - fetchLines: function( lineCallback, endCallback, failCallback ) { - var thisB = this; - this.fetch( function( data ) { - data = new Uint8Array(data); - - var lineIterator = new TextIterator.FromBytes( - { bytes: data, - // only return a partial line at the end - // if we are not operating on a slice of - // the file - returnPartialRecord: !this.end - }); - var line; - while(( line = lineIterator.getline() )) { - lineCallback( line ); - } - - endCallback(); - }, failCallback ); - }, - - readLines: function( offset, length, lineCallback, endCallback, failCallback ) { - var start = this.start + offset, - end = start + length; - var skipFirst = offset != 0; - this.slice( offset, length ) - .fetchLines( - function() { - // skip the first line if we have a - // nonzero offset, because it is probably - // incomplete - if( ! skipFirst ) - lineCallback(); - skipFirst = false; - }, endCallback, failCallback ); - }, - - read: function( offset, length, callback, failCallback ) { - var start = this.start + offset, - end = start + length; - this.slice( offset, length ) - .fetch( callback, failCallback ); - }, - - fetch: function( callback, failCallback ) { - var that = this, - reader = new FileReader(); - reader.onloadend = function(ev) { - callback( that._stringToBuffer( reader.result ) ); - }; - reader.readAsBinaryString( this.blob ); - }, - - _stringToBuffer: function(result) { - if( ! result || ! has('typed-arrays') ) - return null; - - var ba = new Uint8Array( result.length ); - for ( var i = 0; i < ba.length; i++ ) { - ba[i] = result.charCodeAt(i); - } - return ba.buffer; - } - -}); -return FileBlob; -}); \ No newline at end of file diff --git a/client/apollo/js/Model/XHRBlob.js b/client/apollo/js/Model/XHRBlob.js deleted file mode 100644 index 61d5444632..0000000000 --- a/client/apollo/js/Model/XHRBlob.js +++ /dev/null @@ -1,105 +0,0 @@ -define( [ 'dojo/_base/declare', - 'JBrowse/Model/XHRBlob', - 'JBrowse/Store/RemoteBinaryFile' - ], - function( declare, XHR ,RemoteBinaryFileCache ) { -var globalCache = new RemoteBinaryFileCache({ - name: 'XHRBlob', - maxSize: 100000000 // 100MB of file cache -}); - -var XHRBlob = declare( XHR, -/** - * @lends JBrowse.Model.XHRBlob.prototype - */ -{ - - /** - * Blob of binary data fetched with an XMLHTTPRequest. - * - * Adapted by Robert Buels from the URLFetchable object in the - * Dalliance Genome Explorer, which was is copyright Thomas Down - * 2006-2011. - * @constructs - */ - constructor: function(url, start, end, opts) { - if (!opts) { - if (typeof start === 'object') { - opts = start; - start = undefined; - } else { - opts = {}; - } - } - - this.url = url; - this.start = start || 0; - if (end) { - this.end = end; - } - this.opts = opts; - }, - - slice: function(s, l) { - var ns = this.start, ne = this.end; - if (ns && s) { - ns = ns + s; - } else { - ns = s || ns; - } - if (l && ns) { - ne = ns + l - 1; - } else { - ne = ne || l - 1; - } - return new XHRBlob(this.url, ns, ne, this.opts); - }, - - reverseFetchLines: function( lineCallback, endCallback, failCallback ) { - var thisB = this; - this.fetch( function( data ) { - data = new Uint8Array(data); - data = data.reverse(); - - var lineIterator = new TextIterator.FromBytes( - { bytes: data, - // only return a partial line at the end - // if we are not operating on a slice of - // the file - returnPartialRecord: !this.end - }); - var line; - while(( line = lineIterator.getline() )) { - lineCallback( line ); - } - - endCallback(); - }, failCallback ); - }, - - - fetch: function( callback, failCallback ) { - globalCache.get({ - url: this.url, - start: this.start, - end: this.end, - success: callback, - failure: failCallback - }); - }, - - read: function( offset, length, callback, failCallback ) { - var start = this.start + offset, - end = start + length; - - globalCache.get({ - url: this.url, - start: start, - end: end, - success: callback, - failure: failCallback - }); - } -}); -return XHRBlob; -}); \ No newline at end of file diff --git a/client/apollo/js/ProjectionUtils.js b/client/apollo/js/ProjectionUtils.js index b2ee9629e8..6fbd3d9827 100644 --- a/client/apollo/js/ProjectionUtils.js +++ b/client/apollo/js/ProjectionUtils.js @@ -76,11 +76,13 @@ define([ 'dojo/_base/declare', ProjectionUtils.unProjectGFF3 = function(refSeqName,line){ var returnArray ; returnArray = line.split("\t"); - // var sequenceListObject = this.parseSequenceList(refSeqName); var coords = this.unProjectCoordinates(refSeqName,returnArray[3],returnArray[4]); + var sequenceListObject = this.parseSequenceList(refSeqName); + if(sequenceListObject[0].reverse){ + returnArray[6] = this.flipStrand(returnArray[6]); + } returnArray[3] = coords[0]; returnArray[4] = coords[1]; - returnArray[6] = this.flipStrand(returnArray[6]); return returnArray.join("\t") }; diff --git a/client/apollo/js/Store/SeqFeature/GFF3.js b/client/apollo/js/Store/SeqFeature/GFF3.js index c8379bad1f..ddc2caf1ac 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3.js +++ b/client/apollo/js/Store/SeqFeature/GFF3.js @@ -6,10 +6,7 @@ define( [ 'WebApollo/ProjectionUtils', 'WebApollo/Store/SeqFeature/GlobalStatsEstimationMixin', 'JBrowse/Store/SeqFeature/GFF3', - 'JBrowse/Store/SeqFeature/GFF3/Parser', - 'WebApollo/Model/XHRBlob', - 'JBrowse/Util/GFF3', - 'JBrowse/Errors' + 'JBrowse/Store/SeqFeature/GFF3/Parser' ], function( declare, @@ -19,110 +16,16 @@ define( [ ProjectionUtils, GlobalStatsEstimationMixin, GFF3, - Parser, - XHRBlob, - GFF3Parser, - Errors + Parser ) { -return declare([ GFF3 ], +return declare([ GFF3 , GlobalStatsEstimationMixin], /** * @lends JBrowse.Store.SeqFeature.GFF3 */ { - constructor: function( args ) { - console.log('over-ridden constructor with'); - console.log(args); - // this.data = args.blob || - // new XHRBlob( this.resolveUrl( - // this._evalConf(args.urlTemplate) - // ) - // ); - this.data = new XHRBlob( this.resolveUrl( - this._evalConf(args.urlTemplate) - ) - ); - this.features = []; - this._loadFeatures(); - }, - - // _estimateGlobalStats: function(refseq) { - // var deferred = new Deferred(); - // refseq = refseq || this.refSeq; - // var sequenceListObject = ProjectionUtils.parseSequenceList(refseq.name); - // var timeout = this.storeTimeout || 3000; - // var startTime = new Date(); - // - // var statsFromInterval = function( length, callback ) { - // var thisB = this; - // var sampleCenter; - // if (sequenceListObject[0].reverse) { - // sampleCenter = refseq.end * 0.75 + refseq.start * 0.25; - // } - // else { - // sampleCenter = refseq.start * 0.75 + refseq.end * 0.25; - // } - // var start = Math.max( 0, Math.round( sampleCenter - length/2 ) ); - // var end = Math.min( Math.round( sampleCenter + length/2 ), refseq.end ); - // var unprojectedArray = ProjectionUtils.unProjectCoordinates(refseq.name, start, end); - // var unprojectedStart = unprojectedArray[0]; - // var unprojectedEnd = unprojectedArray[1]; - // var features = []; - // this._getFeatures({ - // ref: sequenceListObject[0].name, - // start: unprojectedStart, - // end:unprojectedEnd - // }, - // function( feature ) { - // features.push(feature); - // }, - // function( error ) { - // features = array.filter( - // features, - // function(f) { - // return f.get('start') >= unprojectedStart && f.get('end') <= unprojectedEnd; - // } - // ); - // callback.call( thisB, length, - // { - // featureDensity: features.length / length, - // _statsSampleFeatures: features.length, - // _statsSampleInterval: { ref: refseq.name, start: unprojectedStart, end: unprojectedEnd, length: length } - // }); - // }, - // function( error ) { - // callback.call( thisB, length, null, error ); - // }); - // }; - // - // var maybeRecordStats = function( interval, stats, error ) { - // if( error ) { - // if( error.isInstanceOf(Errors.DataOverflow) ) { - // console.log( 'Store statistics found chunkSizeLimit error, using empty: '+(this.source||this.name) ); - // deferred.resolve( { featureDensity: 0, error: 'global stats estimation found chunkSizeError' } ); - // } - // else { - // deferred.reject( error ); - // } - // } else { - // var refLen = refseq.end - refseq.start; - // if( stats._statsSampleFeatures >= 300 || interval * 2 > refLen || error ) { - // console.log( 'WA Store statistics: '+(this.source||this.name), stats ); - // deferred.resolve( stats ); - // } else if( ((new Date()) - startTime) < timeout ) { - // statsFromInterval.call( this, interval * 2, maybeRecordStats ); - // } else { - // console.log( 'Store statistics timed out: '+(this.source||this.name) ); - // deferred.resolve( { featureDensity: 0, error: 'global stats estimation timed out' } ); - // } - // } - // }; - // - // statsFromInterval.call( this, 100, maybeRecordStats ); - // return deferred; - // }, _loadFeatures: function() { var thisB = this; @@ -181,7 +84,7 @@ return declare([ GFF3 ], line = ProjectionUtils.unProjectGFF3(thisB.refSeq.name,line); // console.log('unprojectED ->['+line+']'); } - console.log(line) ; + // console.log(line) ; lines.push(line); try { parser.addLine(line); @@ -195,12 +98,6 @@ return declare([ GFF3 ], ); }, - // _getFeatures: function( query, featureCallback, finishedCallback, errorCallback ) { - // var thisB = this; - // thisB._deferred.features.then( function() { - // thisB._search( query, featureCallback, finishedCallback, errorCallback ); - // }); - // }, _search: function( query, featureCallback, finishCallback, errorCallback ) { // search in this.features, which are sorted @@ -210,20 +107,13 @@ return declare([ GFF3 ], var converted = this.features; // parse sequenceList from query.ref - var chrName, min, max ; + var chrName ; if(ProjectionUtils.isSequenceList(query.ref)){ var sequenceListObject = ProjectionUtils.parseSequenceList(query.ref); - // unproject start and end - var featureLocationArray = ProjectionUtils.unProjectCoordinates(query.ref, query.start, query.end); - // rebuild the query chrName = sequenceListObject[0].name; - min = featureLocationArray[0]; - max = featureLocationArray[1]; } else{ chrName = query.ref ; - min = query.start ; - max = query.end ; } var refName = this.browser.regularizeReferenceName( chrName ); @@ -235,15 +125,9 @@ return declare([ GFF3 ], } var checkEnd = 'start' in query - ? function(f) { return f.get('end') >= min; } + ? function(f) { return f.get('end') >= query.start; } : function() { return true; }; - // var checkEnd = 'start' in query - // ? function(f) { - // f = ProjectionUtils.projectJSONFeature(f,query.ref); - // return f.get('end') >= min; - // } - // : function() { return true; }; for( ; i max ) + if( f._reg_seq_id != refName || f.get('start') > query.end ) break; if( checkEnd( f ) ) { From 469b478252a26593e17cab6e13a87d373ee2dcc7 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Mon, 11 Dec 2017 20:18:12 -0800 Subject: [PATCH 04/21] made some small changes to the tabix code --- .../apollo/js/Store/SeqFeature/GFF3Tabix.js | 103 +++++++----------- 1 file changed, 42 insertions(+), 61 deletions(-) diff --git a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js index 7ae2e08ba2..c096f1819d 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js +++ b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js @@ -26,50 +26,9 @@ define([ ) { -return declare( [ GlobalStatsEstimationMixin , GFF3Tabix ], +return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], { - constructor: function( args ) { - var thisB = this; - - var tbiBlob = args.tbi || - new XHRBlob( - this.resolveUrl( - this.getConf('tbiUrlTemplate',[]) || this.getConf('urlTemplate',[])+'.tbi' - ) - ); - - var fileBlob = args.file || - new XHRBlob( - this.resolveUrl( this.getConf('urlTemplate',[]) ) - ); - - this.indexedData = new TabixIndexedFile( - { - tbi: tbiBlob, - file: fileBlob, - browser: this.browser, - chunkSizeLimit: args.chunkSizeLimit || 1000000 - }); - - - - - this.getHeader() - .then( function( header ) { - thisB._deferred.features.resolve({success:true}); - thisB._estimateGlobalStats() - .then( - function( stats ) { - thisB.globalStats = stats; - thisB._deferred.stats.resolve( stats ); - }, - lang.hitch( thisB, '_failAllDeferred' ) - ); - }, - lang.hitch( thisB, '_failAllDeferred' ) - ); - }, _getFeatures: function( query, featureCallback, finishedCallback, errorCallback ) { var thisB = this; @@ -88,27 +47,46 @@ return declare( [ GlobalStatsEstimationMixin , GFF3Tabix ], }); thisB.getHeader().then( function() { - var refSeqName = query.ref || thisB.refSeq.name ; + // var refSeqName = query.ref || thisB.refSeq.name ; + var refSeqName = thisB.refSeq.name ; var min, max ; + var reverse = false ; + var chrName ; if(ProjectionUtils.isSequenceList(refSeqName)){ - // var sequenceListObject = ProjectionUtils.parseSequenceList(refSeqName); - var unprojectedArray = ProjectionUtils.unProjectCoordinates(refSeqName,query.start,query.end); - min = unprojectedArray[0]; - max = unprojectedArray[1]; + var sequenceListObject = ProjectionUtils.parseSequenceList(refSeqName); + chrName = sequenceListObject[0].name ; + reverse = sequenceListObject[0].reverse; } else{ - min = query.start ; - max = query.end ; - // chrName = query.ref ; + chrName = query.ref ; } + min = query.start ; + max = query.end ; - console.log(min + ' ' + max); + // console.log(min + ' ' + max); thisB.indexedData.getLines( - refSeqName, + chrName, min, max, function( line ) { + if(ProjectionUtils.isSequenceList(refSeqName)) { + console.log(refSeqName); + console.log(line); + var unprojectedArray = ProjectionUtils.unProjectCoordinates(refSeqName, line.start, line.end); + min = unprojectedArray[0]; + max = unprojectedArray[1]; + line.start = min; + line.end = max; + line.fields[3] = min; + line.fields[4] = max; + if (reverse) { + line.fields[6] = ProjectionUtils.flipStrand(line.fields[6]); + } + console.log('- VS -'); + console.log(line); + } + parser._buffer_feature( thisB.lineToFeature(line) ); }, function() { @@ -133,27 +111,30 @@ return declare( [ GlobalStatsEstimationMixin , GFF3Tabix ], var refSeqName = this.refSeq.name ; var min, max ; + var chrName; if(ProjectionUtils.isSequenceList(refSeqName)){ var sequenceListObject = ProjectionUtils.parseSequenceList(refSeqName); - if(sequenceListObject.reverse){ - strand = -1 * strand ; + chrName = sequenceListObject[0].name ; + if(sequenceListObject[0].reverse){ + strand = ProjectionUtils.flipStrand(strand); + // strand = -1 * strand ; } - var unprojectedArray = ProjectionUtils.unProjectCoordinates(refSeqName,line.start,line.end); - min = unprojectedArray[0]; - max = unprojectedArray[1]; + // var unprojectedArray = ProjectionUtils.unProjectCoordinates(refSeqName,line.start,line.end); + // min = unprojectedArray[0]; + // max = unprojectedArray[1]; } else{ - min = line.start; - max = line.end ; - // chrName = query.ref ; + chrName = query.ref ; } + min = line.start; + max = line.end ; var featureData = { start: min, end: max, strand: strand, child_features: [], - seq_id: line.ref, + seq_id: chrName, attributes: attributes, type: type, source: source, From debcf641c91cc6eba0680700e94a85870e76c390 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Tue, 12 Dec 2017 14:22:40 -0800 Subject: [PATCH 05/21] almost isolated a problem --- .../apollo/js/Store/SeqFeature/GFF3/Parser.js | 1 + .../apollo/js/Store/SeqFeature/GFF3Tabix.js | 54 ++++- client/apollo/js/Store/TabixIndexedFile.js | 225 ++++++++++++++++++ client/apollo/js/Util/TextIterator.js | 59 +++++ 4 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 client/apollo/js/Store/TabixIndexedFile.js create mode 100644 client/apollo/js/Util/TextIterator.js diff --git a/client/apollo/js/Store/SeqFeature/GFF3/Parser.js b/client/apollo/js/Store/SeqFeature/GFF3/Parser.js index b8061d8399..f291a572a1 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3/Parser.js +++ b/client/apollo/js/Store/SeqFeature/GFF3/Parser.js @@ -18,6 +18,7 @@ define([ return declare( [Parser], { constructor: function( args ) { + console.log('using the Apollo parser 2'); lang.mixin( this, { featureCallback: args.featureCallback || function() {}, endCallback: args.endCallback || function() {}, diff --git a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js index c096f1819d..e3fe5941f0 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js +++ b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js @@ -4,10 +4,10 @@ define([ 'dojo/_base/array', 'dojo/Deferred', 'JBrowse/Model/XHRBlob', - 'JBrowse/Store/TabixIndexedFile', + 'WebApollo/Store/TabixIndexedFile', 'WebApollo/Store/SeqFeature/GlobalStatsEstimationMixin', 'WebApollo/ProjectionUtils', - 'JBrowse/Store/SeqFeature/GFF3/Parser', + 'WebApollo/Store/SeqFeature/GFF3/Parser', 'JBrowse/Store/SeqFeature/GFF3Tabix', 'JBrowse/Util/GFF3' ], @@ -29,6 +29,48 @@ define([ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], { + // constructor: function( args ) { + // var thisB = this; + // + // var tbiBlob = args.tbi || + // new XHRBlob( + // this.resolveUrl( + // this.getConf('tbiUrlTemplate',[]) || this.getConf('urlTemplate',[])+'.tbi' + // ) + // ); + // + // var fileBlob = args.file || + // new XHRBlob( + // this.resolveUrl( this.getConf('urlTemplate',[]) ) + // ); + // + // this.indexedData = new TabixIndexedFile( + // { + // tbi: tbiBlob, + // file: fileBlob, + // browser: this.browser, + // chunkSizeLimit: args.chunkSizeLimit || 1000000 + // }); + // + // + // + // + // this.getHeader() + // .then( function( header ) { + // thisB._deferred.features.resolve({success:true}); + // thisB._estimateGlobalStats() + // .then( + // function( stats ) { + // thisB.globalStats = stats; + // thisB._deferred.stats.resolve( stats ); + // }, + // lang.hitch( thisB, '_failAllDeferred' ) + // ); + // }, + // lang.hitch( thisB, '_failAllDeferred' ) + // ); + // }, + _getFeatures: function( query, featureCallback, finishedCallback, errorCallback ) { var thisB = this; @@ -71,8 +113,8 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], max, function( line ) { if(ProjectionUtils.isSequenceList(refSeqName)) { - console.log(refSeqName); - console.log(line); + // console.log(refSeqName); + // console.log(line); var unprojectedArray = ProjectionUtils.unProjectCoordinates(refSeqName, line.start, line.end); min = unprojectedArray[0]; max = unprojectedArray[1]; @@ -83,8 +125,8 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], if (reverse) { line.fields[6] = ProjectionUtils.flipStrand(line.fields[6]); } - console.log('- VS -'); - console.log(line); + // console.log('- VS -'); + // console.log(line); } parser._buffer_feature( thisB.lineToFeature(line) ); diff --git a/client/apollo/js/Store/TabixIndexedFile.js b/client/apollo/js/Store/TabixIndexedFile.js new file mode 100644 index 0000000000..05b5a3e3bc --- /dev/null +++ b/client/apollo/js/Store/TabixIndexedFile.js @@ -0,0 +1,225 @@ +define([ + 'dojo/_base/declare', + 'dojo/_base/array', + 'JBrowse/Util', + 'WebApollo/Util/TextIterator', + 'JBrowse/Store/LRUCache', + 'JBrowse/Errors', + 'JBrowse/Model/XHRBlob', + 'JBrowse/Model/BGZip/BGZBlob', + 'JBrowse/Model/TabixIndex' + ], + function( + declare, + array, + Util, + TextIterator, + LRUCache, + Errors, + XHRBlob, + BGZBlob, + TabixIndex + ) { + +return declare( null, { + + constructor: function( args ) { + this.browser = args.browser; + this.index = new TabixIndex({ blob: new BGZBlob( args.tbi ), browser: args.browser } ); + this.data = new BGZBlob( args.file ); + this.indexLoaded = this.index.load(); + + this.chunkSizeLimit = args.chunkSizeLimit || 2000000; + }, + + getLines: function( ref, min, max, itemCallback, finishCallback, errorCallback ) { + var thisB = this; + var args = Array.prototype.slice.call(arguments); + this.indexLoaded.then(function() { + thisB._fetch.apply( thisB, args ); + }, errorCallback); + }, + + _fetch: function( ref, min, max, itemCallback, finishCallback, errorCallback ) { + errorCallback = errorCallback || function(e) { console.error(e, e.stack); }; + + var chunks = this.index.blocksForRange( ref, min, max); + if ( ! chunks ) { + errorCallback('Error in index fetch ('+[ref,min,max].join(',')+')'); + return; + } + + // toString function is used by the cache for making cache keys + chunks.toString = chunks.toUniqueString = function() { + return this.join(', '); + }; + + // check the chunks for any that are over the size limit. if + // any are, don't fetch any of them + for( var i = 0; i this.chunkSizeLimit ) { + errorCallback( new Errors.DataOverflow('Too much data. Chunk size '+Util.commifyNumber(size)+' bytes exceeds chunkSizeLimit of '+Util.commifyNumber(this.chunkSizeLimit)+'.' ) ); + return; + } + } + + var fetchError; + try { + this._fetchChunkData( + chunks, + ref, + min, + max, + itemCallback, + finishCallback, + errorCallback + ); + } catch( e ) { + errorCallback( e ); + } + }, + + _fetchChunkData: function( chunks, ref, min, max, itemCallback, endCallback, errorCallback ) { + var thisB = this; + + if( ! chunks.length ) { + endCallback(); + return; + } + + var allItems = []; + var chunksProcessed = 0; + + var cache = this.chunkCache = this.chunkCache || new LRUCache({ + name: 'TabixIndexedFileChunkedCache', + fillCallback: dojo.hitch( this, '_readChunkItems' ), + sizeFunction: function( chunkItems ) { + return chunkItems.length; + }, + maxSize: 100000 // cache up to 100,000 items + }); + + var regRef = this.browser.regularizeReferenceName( ref ); + + var haveError; + array.forEach( chunks, function( c ) { + cache.get( c, function( chunkItems, e ) { + if( e && !haveError ) + errorCallback( e ); + if(( haveError = haveError || e )) { + return; + } + + for( var i = 0; i< chunkItems.length; i++ ) { + // var item = chunkItems[chunkItems.length - i - 1]; + var item = chunkItems[i]; + if( item._regularizedRef == regRef ) { + // on the right ref seq + if( item.start > max ) // past end of range, can stop iterating + break; + else if( item.end >= min ) // must be in range + itemCallback( item ); + } + } + if( ++chunksProcessed == chunks.length ) { + endCallback(); + } + }); + }); + }, + + _readChunkItems: function( chunk, callback ) { + var thisB = this; + var items = []; + + thisB.data.read(chunk.minv.block, chunk.maxv.block - chunk.minv.block + 1, function( data ) { + data = new Uint8Array(data); + //console.log( 'reading chunk %d compressed, %d uncompressed', chunk.maxv.block-chunk.minv.block+65536, data.length ); + var lineIterator = new TextIterator.FromBytes({ bytes: data, offset: 0 }); + try { + thisB._parseItems( + lineIterator, + function(i) { items.push(i); }, + function() { callback(items); } + ); + } catch( e ) { + callback( null, e ); + } + }, + function(e) { + callback( null, e ); + }); + }, + + _parseItems: function( lineIterator, itemCallback, finishCallback ) { + var that = this; + var itemCount = 0; + + var maxItemsWithoutYielding = 300; + while ( true ) { + // if we've read no more than a certain number of items this cycle, read another one + if( itemCount <= maxItemsWithoutYielding ) { + var item = this.parseItem( lineIterator ); + if( item ) { + itemCallback( item ); + itemCount++; + } + else { + finishCallback(); + return; + } + } + // if we're not done but we've read a good chunk of + // items, schedule the rest of our work in a timeout to continue + // later, avoiding blocking any UI stuff that needs to be done + else { + window.setTimeout( function() { + that._parseItems( lineIterator, itemCallback, finishCallback ); + }, 1); + return; + } + } + }, + + parseItem: function( iterator ) { + var metaChar = this.index.metaChar; + var line, item; + do { + line = iterator.getline(); + } while( line && ( line.charAt(0) == metaChar // meta line, skip + || line.charAt( line.length - 1 ) != "\n" // no newline at the end, incomplete + || ! ( item = this.tryParseLine( line ) ) // line could not be parsed + ) + ); + + if( line && item ) + return item; + + return null; + }, + + tryParseLine: function( line ) { + try { + return this.parseLine( line ); + } catch(e) { + //console.warn('parse failed: "'+line+'"'); + return null; + } + }, + + parseLine: function( line ) { + var fields = line.split( "\t" ); + fields[fields.length-1] = fields[fields.length-1].replace(/\n$/,''); // trim off the newline + var item = { // note: index column numbers are 1-based + ref: fields[this.index.columnNumbers.ref-1], + _regularizedRef: this.browser.regularizeReferenceName( fields[this.index.columnNumbers.ref-1] ), + start: parseInt(fields[this.index.columnNumbers.start-1]), + end: parseInt(fields[this.index.columnNumbers.end-1]), + fields: fields + }; + return item; + } + +}); +}); diff --git a/client/apollo/js/Util/TextIterator.js b/client/apollo/js/Util/TextIterator.js new file mode 100644 index 0000000000..12736ce780 --- /dev/null +++ b/client/apollo/js/Util/TextIterator.js @@ -0,0 +1,59 @@ +/** + * Classes to iterate over records in an array-like structure of bytes (FromBytes). + */ + +define([ + ], + function( + ) { + +var FromBytes = function(args) { + this.bytes = args.bytes; + this.offset = args.offset || 0; + this.length = args.length || this.bytes.length; + this._recordSeparator = (args.inputRecordSeparator || "\n").charCodeAt(0); + this.returnPartialRecord = args.returnPartialRecord; +}; + +FromBytes.prototype.getOffset = function() { + return this.offset; +}; + +// get a line of text, properly decoding UTF-8 +FromBytes.prototype.getline = function() { + var bytes = this.bytes; + var i = this.offset; + + var line = []; + while( i < this.length ) { + var c1 = bytes[i], c2, c3; + if (c1 < 128) { + line.push( String.fromCharCode(c1) ); + i++; + if( c1 == this._recordSeparator ) { + this.offset = i; + return line.join(''); + } + } else if (c1 > 191 && c1 < 224) { + c2 = bytes[i + 1]; + line.push( String.fromCharCode(((c1 & 31) << 6) | (c2 & 63)) ); + i += 2; + } else { + c2 = bytes[i + 1]; + c3 = bytes[i + 2]; + line.push( String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)) ); + i += 3; + } + } + + // did not get a full line + this.offset = i; + // return our partial line if we are set to return partial records + return this.returnPartialRecord ? line.join('') : null; +}; + +return { + FromBytes: FromBytes +}; + +}); \ No newline at end of file From 2ffd8d6109681174686a5df88e5641a193c5d26e Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Wed, 13 Dec 2017 17:49:18 -0800 Subject: [PATCH 06/21] added a separate projected HTMLFeatures --- client/apollo/js/TrackConfigTransformer.js | 4 + .../js/View/Track/DraggableHTMLFeatures.js | 3 + .../Track/DraggableProjectedHTMLFeatures.js | 1818 +++++++++++++++++ client/apollo/js/View/Track/HTMLFeatures.js | 1 + 4 files changed, 1826 insertions(+) create mode 100644 client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js diff --git a/client/apollo/js/TrackConfigTransformer.js b/client/apollo/js/TrackConfigTransformer.js index 9f741363cd..b9a4341b5a 100644 --- a/client/apollo/js/TrackConfigTransformer.js +++ b/client/apollo/js/TrackConfigTransformer.js @@ -17,6 +17,10 @@ constructor: function( args ) { trackConfig.type = "WebApollo/View/Track/DraggableHTMLFeatures"; }; + this.transformers["WebApollo/View/Track/HTMLFeatures"] = function(trackConfig) { + trackConfig.type = "WebApollo/View/Track/DraggableProjectedHTMLFeatures"; + }; + this.transformers["JBrowse/View/Track/CanvasFeatures"] = function(trackConfig) { trackConfig.type = "WebApollo/View/Track/WebApolloCanvasFeatures"; }; diff --git a/client/apollo/js/View/Track/DraggableHTMLFeatures.js b/client/apollo/js/View/Track/DraggableHTMLFeatures.js index f522398c76..e418e74dc2 100644 --- a/client/apollo/js/View/Track/DraggableHTMLFeatures.js +++ b/client/apollo/js/View/Track/DraggableHTMLFeatures.js @@ -692,7 +692,9 @@ var draggableTrack = declare( HTMLFeatureTrack, */ renderFeature: function(feature, uniqueId, block, scale, labelScale, descriptionScale, containerStart, containerEnd, rclass, clsName ) { + console.log('BBBBBB 1'); var featdiv = this.inherited( arguments ); + console.log('BBBBBB 2'); if( featdiv ) { // just in case featDiv doesn't actually get created var $featdiv = $(featdiv); @@ -727,6 +729,7 @@ var draggableTrack = declare( HTMLFeatureTrack, displayStart, displayEnd, block ) { var subfeatdiv = this.inherited( arguments ); + console.log('rendering subfeature') if (subfeatdiv) { // just in case subFeatDiv doesn't actually get created var $subfeatdiv = $(subfeatdiv); // adding pointer to track for each subfeatdiv diff --git a/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js b/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js new file mode 100644 index 0000000000..471c9b78ff --- /dev/null +++ b/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js @@ -0,0 +1,1818 @@ +define( [ + 'dojo/_base/declare', + 'dojo/_base/array', + 'WebApollo/View/Track/HTMLFeatures', + 'WebApollo/FeatureSelectionManager', + 'WebApollo/View/Projection/FASTA', + 'dijit/Menu', + 'dijit/MenuItem', + 'dijit/CheckedMenuItem', + 'dijit/MenuSeparator', + 'dijit/PopupMenuItem', + 'dijit/Dialog', + 'dijit/Tooltip', + 'dojo/dom-construct', + 'dojo/query', + 'jquery', + 'jqueryui/draggable', + 'JBrowse/Util', + 'JBrowse/Model/SimpleFeature', + 'WebApollo/SequenceOntologyUtils' + ], + function( declare, + array, + HTMLFeatureTrack, + FeatureSelectionManager, + FASTAView, + dijitMenu, + dijitMenuItem, + dijitCheckedMenuItem, + dijitMenuSeparator, + dijitPopupMenuItem, + dijitDialog, + dijitTooltip, + domConstruct, + query, + $, + draggable, + Util, + SimpleFeature, + SeqOnto ) { + +var debugFrame = false ; + +var draggableTrack = declare( HTMLFeatureTrack, + +{ + // so is dragging + dragging: false, + + _defaultConfig: function() { + return Util.deepUpdate( + dojo.clone( this.inherited(arguments) ), + { + style: { + // className: "{type}", // feature classname gets set to feature.get('type') + className: "container-16px", + renderClassName: "gray-center-30pct annot-apollo", + arrowheadClass: "webapollo-arrowhead", + subfeatureClasses: { + UTR: "webapollo-UTR", + CDS: "webapollo-CDS", + exon: "container-100pct", + intron: null, + wholeCDS: null, + start_codon: null, + stop_codon: null, + match_part: "darkblue-80pct" + }, + + // renderClassName: 'DraggableFeatureTrack' ??? + // setting minSubfeatureWidth to 1 insures subfeatures will almost always get drawn, + minSubfeatureWidth: 1, + centerChildrenVertically: false + }, + events: { + // need to map click to a null-op, to override default JBrowse click behavior for click on features + // (JBrowse default is feature detail popup) + click: function(event) { + // not quite a null-op, also need to suprress propagation of click recursively up through parent divs, + // in order to stop default JBrowse behavior for click on tracks (which is to recenter view at click point) + event.stopPropagation(); + } + // WebApollo can't set up mousedown --> onFeatureMouseDown() in config.events, + // because dojo.on used by JBrowse config-based event setup doesn't play nice with + // JQuery event retriggering via _mousedown() for feature drag bootstrapping + // also, JBrowse only sets these events for features, and WebApollo needs them to trigger for subfeatures as well + // , mousedown: dojo.hitch( this, 'onFeatureMouseDown' ), + // , dblclick: dojo.hitch( this, 'onFeatureDoubleClick' ) + } + } + ); + }, + + constructor: function( args ) { + //var coordinate = new Coordinate(3,2,) + //Coordinate.spitOutSomething; + var thisB = this; + this.gview = this.browser.view; + // get a handle to on the main WA object + this.browser.getPlugin( 'WebApollo', dojo.hitch( this, function(p) { + this.webapollo = p; + })); + + // DraggableFeatureTracks all share the same FeatureSelectionManager + // if want subclasses to have different selection manager, + // call this.setSelectionManager in subclass (after calling parent constructor) + this.setSelectionManager( this.webapollo.featSelectionManager ); + + // CSS class for selected features + // override if want subclass to have different CSS class for selected features + this.selectionClass = "selected-feature"; + + // DraggableFeatureTrack.selectionManager.addListener(this); + + this.last_whitespace_mousedown_loc = null; + this.last_whitespace_mouseup_time = new Date(); // dummy timestamp + this.prev_selection = null; + + this.verbose = false; + this.verbose_selection = false; + this.verbose_selection_notification = false; + this.verbose_drag = false; + this.drag_enabled = true; + + this.feature_context_menu = null; + + /** hack to determine which tracks to apply edge matching to + would rather do a check for whether track is instance of DraggableHTMLFeatures (or possibly HTMLFeatures), + but use of dojo.declare() for classes means track object's class is actually base Object. + */ + this.edge_matching_enabled = true; + + + dojo.subscribe("/jbrowse/v1/n/tracks/redraw", function(data){ + setTimeout(function(){ + thisB.updateFeatures(); + }, 100); + }); + }, + + updateFeatures: function( args ) { + //console.log("updateFeatures"); + + var thisB = this; + + var divQuery = "div.feature"; // by default, paint all feature divs + + // apply introns to all feature tracks + query(divQuery).forEach(function(featureNode, index, arr){ + + // scan and insert introns, where applicable + // TODO: add back when we do folding + // thisB.insertFolds(featureNode); + // thisB.insertEdges(featureNode); + }); + + }, + + getApollo: function(){ + return window.parent; + }, + + // TODO: provided so that we over-write with the correct FASTAView object, probably a more efficient way to do this + _renderUnderlyingReferenceSequence: function( track, f, featDiv, container ) { + // render the sequence underlying this feature if possible + var field_container = dojo.create('div', { className: 'field_container feature_sequence' }, container ); + dojo.create( 'h2', { className: 'field feature_sequence', innerHTML: 'Region sequence', title: 'reference sequence underlying this '+(f.get('type') || 'feature') }, field_container ); + var valueContainerID = 'feature_sequence'+this._uniqID(); + var valueContainer = dojo.create( + 'div', { + id: valueContainerID, + innerHTML: '
Loading...
', + className: 'value feature_sequence' + }, field_container); + var maxSize = this.config.maxFeatureSizeForUnderlyingRefSeq; + if( maxSize < (f.get('end') - f.get('start')) ) { + valueContainer.innerHTML = 'Not displaying underlying reference sequence, feature is longer than maximum of '+Util.humanReadableNumber(maxSize)+'bp'; + } else { + track.browser.getStore('refseqs', dojo.hitch(this,function( refSeqStore ) { + valueContainer = dojo.byId(valueContainerID) || valueContainer; + if( refSeqStore ) { + refSeqStore.getReferenceSequence( + { + ref: this.refSeq.name, + start: f.get('start'), + end: f.get('end') + }, + // feature callback + dojo.hitch( this, function( seq ) { + valueContainer = dojo.byId(valueContainerID) || valueContainer; + valueContainer.innerHTML = ''; + // the HTML is rewritten by the dojo dialog + // parser, but this callback may be called either + // before or after that happens. if the fetch by + // ID fails, we have come back before the parse. + var textArea = new FASTAView({ track: this, width: 62, htmlMaxRows: 10 }) + .renderHTML( + { ref: this.refSeq.name, + start: f.get('start'), + end: f.get('end'), + strand: f.get('strand'), + type: f.get('type') + }, + f.get('strand') == -1 ? Util.revcom(seq) : seq, + valueContainer + ); + }), + // end callback + function() {}, + // error callback + dojo.hitch( this, function() { + valueContainer = dojo.byId(valueContainerID) || valueContainer; + valueContainer.innerHTML = 'reference sequence not available'; + }) + ); + } else { + valueContainer.innerHTML = 'reference sequence not available'; + } + })); + } + }, + + insertEdges: function(featureNode) { + + function handleLeft(a,b){ + alert(a); + } + + // ignore if we have already processed this node + // get the subfeatures nodes (only immediate children) + var subNodesX = query('> .subfeature', featureNode); + + // filter nodes - eliminate nodes that are splice sites (for Apollo) + var subNodesFmin = []; + var subNodesFmax = []; + for (var i = 0; i < subNodesX.length; i++) { + var subNodeX = subNodesX[i]; + if(subNodeX.subfeature.afeature){ + if(subNodeX.subfeature.afeature.location.is_fmin_partial){ + console.log("feature has an fmin partial"); + subNodesFmin.push(subNodeX); + } + if(subNodeX.subfeature.afeature.location.is_fmax_partial){ + console.log("feature has an fmax partial"); + subNodesFmax.push(subNodeX); + } + } + } + + if (subNodesFmin.length == 0 && subNodesFmax.length==0) { + return; + } + + for (var i = 0; i < subNodesFmin.length; i++) { + subNodesFmin[i].left = dojo.getStyle(subNodesFmin[i], "left"); + subNodesFmin[i].width = dojo.getStyle(subNodesFmin[i], "width"); + } + for (var i = 0; i < subNodesFmax.length; i++) { + subNodesFmax[i].left = dojo.getStyle(subNodesFmax[i], "left"); + subNodesFmax[i].width = dojo.getStyle(subNodesFmax[i], "width"); + } + var width = 100 ; + var height = 30; + + var priorMap = {}; + + for (var i = 0; i < subNodesFmin.length ; ++i) { + var leftNode = subNodesFmin[i]; + if(leftNode.subfeature.afeature){ + var leftEdge = leftNode.subfeature.afeature.location.fmin; + var priorSequence = JSON.parse(leftNode.subfeature.afeature.location.fmin_data); + // var rightEdge = leftNode.subfeature.afeature.location.fmax; + // var left = subNodesFmin[i].left + subNodesFmin[i].width; + var left = subNodesFmin[i].left + 1 ; + + var strLeft = ""; + + var leftValue = Util.addCommas(leftEdge) ; + // var rightValue = Util.addCommas(rightEdge) ; + + // var projectionId = "projectionLabels"+leftValue+""+rightValue; + var projectionId = "projectionEdgeLeftLabel"+leftValue; + + console.log(projectionId+ " left:"+left); + + + + if(left && !document.getElementById(projectionId)){ + priorMap[projectionId] = priorSequence; + strLeft += ""; + strLeft += ''; + strLeft += ""; + } + } + + + // console.log('str length: '+str.length + ' for ' + str); + if(strLeft.length >0){ + domConstruct.place(strLeft, featureNode); + if(left && document.getElementById(projectionId)){ + var element1 = document.getElementById(projectionId); + var thisSeq = priorMap[projectionId]; + element1.onclick = function(){ + var myDialog = new dijitDialog({ + title: "Partial Element", + content: 'Genomic element continued on '+thisSeq.name +'.', + // '.
', + style: "width: 300px" + }); + myDialog.show(); + } + } + } + } + + for (var i = 0; i < subNodesFmax.length ; ++i) { + var rightNode = subNodesFmax[i]; + if(rightNode.subfeature.afeature){ + var rightEdge = rightNode.subfeature.afeature.location.fmax; + var nextSequence = JSON.parse(rightNode.subfeature.afeature.location.fmax_data); + var right = subNodesFmax[i].left + subNodesFmax[i].width - 10; + // var right = subNodesFmax[i].left + + 1 ; + + var strRight = ""; + + var rightValue = Util.addCommas(rightEdge) ; + // var rightValue = Util.addCommas(rightEdge) ; + + // var projectionId = "projectionLabels"+rightValue+""+rightValue; + var projectionId = "projectionEdgeRightLabel"+rightValue; + + console.log(projectionId+ " right:"+right); + + if(right && !document.getElementById(projectionId)){ + priorMap[projectionId] = nextSequence ; + strRight += ""; + strRight += ''; + strRight += ""; + } + } + + + // console.log('str length: '+str.length + ' for ' + str); + if(strRight.length >0){ + domConstruct.place(strRight, featureNode); + if(right && document.getElementById(projectionId)){ + element1 = document.getElementById(projectionId); + thisSeq = priorMap.projectionId; + element1.onclick = function(){ + var myDialog = new dijitDialog({ + title: "Partial Element", + content: 'Genomic element continued on '+thisSeq.name +'.', + // '.
', + style: "width: 300px" + }); + myDialog.show(); + } + } + } + } + + + }, + + insertFolds: function(featureNode) { + + var intronCount = 0; + + // ignore if we have already processed this node + if (this.refSeq.name.indexOf('location')>0 ) { + + // get the subfeatures nodes (only immediate children) + var subNodesX = query('> .subfeature', featureNode); + + // filter nodes - eliminate nodes that are splice sites (for Apollo) + var subNodes = []; + for (var i = 0; i < subNodesX.length; i++) { + var attr = dojo.attr(subNodesX[i], "class"); + if (attr.indexOf("splice-site") === -1) + subNodes.push(subNodesX[i]); + } + + if (subNodes.length == 0) { + return; + } + + // identify directionality + var classAttr = dojo.attr(featureNode, "class"); + + //extract some left & width - more convient to access + for (var i = 0; i < subNodes.length; i++) { + subNodes[i].left = dojo.getStyle(subNodes[i], "left"); + subNodes[i].width = dojo.getStyle(subNodes[i], "width"); + } + + /* debug display subfeature list + console.dir(subNodes); + for(var i=0; i < subNodes.length;i++) { + console.log(i + " subfeature left,width: "+subNodes[i].left+", "+subNodes[i].width); + } + */ + + // sort the subfeatures + if (subNodes.length >= 2) { + subNodes.sort(function (a, b) { + return a.left - b.left; + }); + // insert introns between subfeature gaps + for (var i = 0; i < subNodes.length - 1; ++i) { + var leftNode = subNodes[i]; + var rightNode = subNodes[i+1]; + var regionFolded = false ; + var folds = null ; + if(leftNode.subfeature.afeature && rightNode.subfeature.afeature){ + var leftEdge = leftNode.subfeature.afeature.location.fmax ; + var rightEdge = rightNode.subfeature.afeature.location.fmin ; + regionFolded = this.getApollo().regionContainsFolds(leftEdge,rightEdge,this.refSeq.name); + folds = this.getApollo().getFoldsForRegion(this.refSeq.name,leftEdge,rightEdge); + } + // TODO: just use 'folds' at some point + if (regionFolded) { + var subLeft = subNodes[i].left + subNodes[i].width; + var subWidth = subNodes[i + 1].left - (subNodes[i].left + subNodes[i].width); + + var left = subLeft; + var width = subWidth; + + var height = "100%"; + var totalHeight = "2000px"; + + var str = ""; + + // this is the back divider line . . . + // console.log("width: " + width); + width = width < 1 ? 1 : width; + var dividerId = 'projectionFoldDivider'+left; + if(!document.getElementById(dividerId)){ + var strokeWidth = 5.0 / this.scale ; + + str += ""; + str += ""; + str += ""; + } + + + // console.log('scale: '+this.scale); + // console.log(this.gview.pxPerBp); + // console.log(this.gview.curZoom); + if(this.scale >=2){ + var fold = folds[0]; + var leftValue = Util.addCommas(fold.left) ; + var rightValue = Util.addCommas(fold.right) ; + + var projectionId = "projectionLabels"+leftValue+""+rightValue; + + var leftX = width / 2.0 - (width * 0.05) - (leftValue.length *10) ; + var rightX = width / 2.0 + (width * 0.02) ; + + console.log(leftX + " <-> "+rightX ); + + if(leftX && rightX && !document.getElementById(projectionId)){ + str += ""; + // draw the right arrow + // str += '' + // console.log('leftx: '+leftX); + str += '' + // console.log('leftx: '+leftX); + str += ''; + str += leftValue ; + str += ''; + // str += '' + str += '' + str += ''; + str += rightValue ; + str += ''; + str += ""; + } + } + + + // console.log('str length: '+str.length + ' for ' + str); + if(str.length >0){ + domConstruct.place(str, featureNode); + } + + + intronCount++; + + } + } + } + + if (intronCount) { + // mark that we have processed this node + dojo.addClass(featureNode, "has-neat-introns"); + } + } + }, + + loadSuccess: function(trackInfo) { + /* if subclass indicates it has custom context menu, do not initialize default feature context menu */ + if (! this.has_custom_context_menu) { + this.initFeatureContextMenu(); + this.initFeatureDialog(); + } + this.inherited( arguments ); + }, + + setSelectionManager: function(selman) { + if (this.selectionManager) { + this.selectionManager.removeListener(this); + } + this.selectionManager = selman; + this.selectionManager.addListener(this); + return selman; + }, + + /** + * only called once, during track setup ??? + * + * doublclick in track whitespace is used by JBrowse for zoom + * but WebApollo/JBrowse uses single click in whitespace to clear selection + * + * so this sets up mousedown/mouseup/doubleclick + * kludge to restore selection after a double click to whatever selection was before + * initiation of doubleclick (first mousedown/mouseup) + * + */ + setViewInfo: function(genomeView, numBlocks, + trackDiv, labelDiv, + widthPct, widthPx, scale) { + this.inherited( arguments ); + + var $div = $(this.div); + var track = this; + + // this.scale = scale; // scale is in pixels per base + + // setting up mousedown and mouseup handlers to enable click-in-whitespace to clear selection + // (without conflicting with JBrowse drag-in-whitespace to scroll) + $div.bind('mousedown', function(event) { + var target = event.target; + if (! (target.feature || target.subfeature)) { + track.last_whitespace_mousedown_loc = [ event.pageX, event.pageY ]; + } + } ); + $div.bind('mouseup', function(event) { + var target = event.target; + if (! (target.feature || target.subfeature)) { // event not on feature, so must be on whitespace + var xup = event.pageX; + var yup = event.pageY; + // if click in whitespace without dragging (no movement between mouse down and mouse up, + // and no shift modifier, + // then deselect all + if (this.verbose_selection) { console.log("mouse up on track whitespace"); } + var eventModifier = event.shiftKey || event.altKey || event.metaKey || event.ctrlKey; + if (track.last_whitespace_mousedown_loc && + xup === track.last_whitespace_mousedown_loc[0] && + yup === track.last_whitespace_mousedown_loc[1] && + (! eventModifier )) { + var timestamp = new Date(); + var prev_timestamp = track.last_whitespace_mouseup_time; + track.last_whitespace_mouseup_time = timestamp; + // if less than half a second, probably a doubleclick (or triple or more click...) + var probably_doubleclick = ((timestamp.getTime() - prev_timestamp.getTime()) < 500); + if (probably_doubleclick) { + if (this.verbose_selection) { console.log("mouse up probably part of a doubleclick"); } + // don't record selection state, want to keep prev_selection set + // to selection prior to first mouseup of doubleclick + } + else { + track.prev_selection = track.selectionManager.getSelection(); + if (this.verbose_selection) { + console.log("recording prev selection"); + console.log(track.prev_selection); + } + } + if (this.verbose_selection) { console.log("clearing selection"); } + track.selectionManager.clearAllSelection(); + } + else { + track.prev_selection = null; + } + } + // regardless of what element it's over, mouseup clears out tracking of mouse down + track.last_whitespace_mousedown_loc = null; + } ); + // kludge to restore selection after a double click to whatever selection was before + // initiation of doubleclick (first mousedown/mouseup) + $div.bind('dblclick', function(event) { + var target = event.target; + // because of dblclick bound to features, will only bubble up to here on whitespace, + // but doing feature check just to make sure + if (! (target.feature || target.subfeature)) { + if (this.verbose_selection) { + console.log("double click on track whitespace"); + console.log("restoring selection after double click"); + console.log(track.prev_selection); + } + if (track.prev_selection) { + var plength = track.prev_selection.length; + // restore selection + for (var i = 0; i bs ) { return 1; } + else if ( as < bs ) { return -1; } + else { return 0; /* shouldn't fall through to here */ } + }, + + + /** + * if feature has translated region (CDS, wholeCDS, start_codon, ???), + * reworks feature's subfeatures for more annotation-editing-friendly selection + * + * Assumes: + * if translated, will either have + * CDS-ish term for each coding segment + * wholeCDS from start of translation to end of translation (so already pre-processed) + * mutually exclusive (either have CDS, or wholeCDS, but not both) + * if wholeCDS present, then pre-processed (no UTRs) + * if any exon-ish types present, then _all_ exons are present with exon-ish types + */ + _processTranslation: function( feature ) { + var track = this; + + var feat_type = feature.get('type'); + + // most very dense genomic feature tracks do not have CDS. Trying to minimize overhead for that case -- + // keep list of types that NEVER have CDS children (match, alignment, repeat, etc.) + // (WARNING in this case not sorting, but sorting (currently) only needed for features with CDS (for reading frame calcs)) + if (SeqOnto.neverHasCDS[feat_type]) { + feature.normalized = true; + return; + } + var subfeats = feature.get('subfeatures'); + + // var cds = subfeats.filter( function(feat) { return feat.get('type') === 'CDS'; } ); + var cds = subfeats.filter( function(feat) { + return SeqOnto.cdsTerms[feat.get('type')]; + } ); + var wholeCDS = subfeats.filter( function(feat) { return feat.get('type') === 'wholeCDS'; } ); + + // most very dense genomic feature tracks do not have CDS. Trying to minimize overhead for that case -- + // if no CDS, no wholeCDS, consider normalized + // (WARNING in this case not sorting, but sorting (currently) only needed for features with CDS (for reading frame calcs)) + // + if (cds.length === 0 && wholeCDS.length === 0) { + feature.normalized = true; + return; + } + + var newsubs; + // wholeCDS is specific to WebApollo, if seen can assume no CDS, and UTR/exon already normalized + if (wholeCDS.length > 0) { + // extract wholecds from subfeats, then sort subfeats + feature.wholeCDS = wholeCDS[0]; + newsubs = subfeats.filter( function(feat) { return feat.get('type') !== 'wholeCDS'; } ); + } + + // if has a CDS, remove CDS from subfeats and sort exons + else if (cds.length > 0) { + cds.sort(this._subfeatSorter); + var cdsmin = cds[0].get('start'); + var cdsmax = cds[cds.length-1].get('end'); + feature.wholeCDS = new SimpleFeature({ parent: feature, + data: { start: cdsmin, end: cdsmax, type: 'wholeCDS', + strand: feature.get('strand') } + } ); + var hasExons = false; + for (var i=0; i 0, guaranteed to have at least one CDS + var exonCount = 0; + var prevStart, prevEnd; + // scan through sorted subfeats, joining abutting UTR/CDS regions + for (var i=0; i displayStart) && (subStart < displayEnd)); + var render = subDiv && (subEnd > displayStart) && (subStart < displayEnd); + + // look for UTR and CDS subfeature class mapping from trackData + // if can't find, then default to parent feature class + "-UTR" or "-CDS" + if( render ) { // subfeatureClases defaults set in this._defaultConfig + if (!UTRclass) { + UTRclass = this.config.style.subfeatureClasses["UTR"]; + } + CDSclass = this.config.style.subfeatureClasses["CDS"]; + } + + // if ((subEnd <= displayStart) || (subStart >= displayEnd)) { return undefined; } + + var segDiv; + // console.log("render sub frame"); + // whole exon is untranslated (falls outside wholeCDS range, or no CDS info found) + if( (cdsMin === undefined && cdsMax === undefined) || + (cdsMax <= subStart || cdsMin >= subEnd)) { + if( render ) { + segDiv = document.createElement("div"); + // not worrying about appending "plus-"/"minus-" based on strand yet + dojo.addClass(segDiv, "subfeature"); + dojo.addClass(segDiv, UTRclass); + if (Util.is_ie6) segDiv.appendChild(document.createComment()); + segDiv.style.cssText = + "left: " + (100 * ((subStart - subStart) / subLength)) + "%;" + + "width: " + (100 * ((subEnd - subStart) / subLength)) + "%;"; + subDiv.appendChild(segDiv); + } + } + + /* + Frame is calculated as (3 - ((length-frame) mod 3)) mod 3. + (length-frame) is the length of the previous feature starting at the first whole codon (and thus the frame subtracted out). + (length-frame) mod 3 is the number of bases on the 3' end beyond the last whole codon of the previous feature. + 3-((length-frame) mod 3) is the number of bases left in the codon after removing those that are represented at the 3' end of the feature. + (3-((length-frame) mod 3)) mod 3 changes a 3 to a 0, since three bases makes a whole codon, and 1 and 2 are left unchanged. + */ + // whole exon is translated + else if (cdsMin <= subStart && cdsMax >= subEnd) { + var overhang = priorCdsLength % 3; // number of bases overhanging from previous CDS + var absFrame, cdsFrame, initFrame + var relFrame = (3 - (priorCdsLength % 3)) % 3; + if (reverse) { + initFrame = (cdsMax ) % 3; + absFrame = (subEnd ) % 3; + cdsFrame = ( (absFrame - relFrame) + 3 ) % 3; + cdsFrame = this.handleReverseStrandOffset(cdsFrame); + } + else { + initFrame = cdsMin % 3; + absFrame = (subStart % 3); + cdsFrame = (absFrame + relFrame) % 3; + } + if (debugFrame) { + console.log("whole exon: " + subStart + " -- ", subEnd, " initFrame: ", initFrame, + ", overhang: " + overhang + ", relFrame: ", relFrame, ", absFrame: ", absFrame, + ", cdsFrame: " + cdsFrame); + } + + if (render) { + segDiv = document.createElement("div"); + // not worrying about appending "plus-"/"minus-" based on strand yet + dojo.addClass(segDiv, "subfeature"); + dojo.addClass(segDiv, CDSclass); + if (Util.is_ie6) segDiv.appendChild(document.createComment()); + segDiv.style.cssText = + "left: " + (100 * ((subStart - subStart) / subLength)) + "%;" + + "width: " + (100 * ((subEnd - subStart) / subLength)) + "%;"; + dojo.addClass(segDiv, "cds-frame" + cdsFrame); + subDiv.appendChild(segDiv); + } + priorCdsLength += subLength; + } + // partial translation of exon + else { + // calculate 5'UTR, CDS segment, 3'UTR + var cdsSegStart = Math.max(cdsMin, subStart); + var cdsSegEnd = Math.min(cdsMax, subEnd); + var overhang = priorCdsLength % 3; // number of bases overhanging + var absFrame, cdsFrame, initFrame; + if (priorCdsLength > 0) { + var relFrame = (3 - (priorCdsLength % 3)) % 3; + if (reverse) { + initFrame = (cdsMax) % 3; + absFrame = (subEnd ) % 3; + cdsFrame = (3 + absFrame - relFrame) % 3; + cdsFrame = this.handleReverseStrandOffset(cdsFrame); + } + else { + // cdsFrame = (subStart + ((3 - (priorCdsLength % 3)) % 3)) % 3; + initFrame = cdsMin % 3; + absFrame = (subStart % 3); + cdsFrame = (absFrame + relFrame) % 3; + } + if (debugFrame) { console.log("partial exon: " + subStart + ", initFrame: " + (cdsMin % 3) + + ", overhang: " + overhang + ", relFrame: " + relFrame + ", subFrame: " + (subStart % 3) + + ", cdsFrame: " + cdsFrame); } + } + else { // actually shouldn't need this? -- if priorCdsLength = 0, then above conditional collapses down to same calc... + if (reverse) { + cdsFrame = (cdsMax) % 3; + cdsFrame = this.handleReverseStrandOffset(cdsFrame); + } + else { + cdsFrame = cdsMin % 3; + } + } + + var utrStart; + var utrEnd; + // make left UTR (if needed) + if (cdsMin > subStart) { + utrStart = subStart; + utrEnd = cdsSegStart; + if (render) { + segDiv = document.createElement("div"); + // not worrying about appending "plus-"/"minus-" based on strand yet + dojo.addClass(segDiv, "subfeature"); + dojo.addClass(segDiv, UTRclass); + if (Util.is_ie6) segDiv.appendChild(document.createComment()); + segDiv.style.cssText = + "left: " + (100 * ((utrStart - subStart) / subLength)) + "%;" + + "width: " + (100 * ((utrEnd - utrStart) / subLength)) + "%;"; + subDiv.appendChild(segDiv); + } + } + if (render) { + // make CDS segment + segDiv = document.createElement("div"); + // not worrying about appending "plus-"/"minus-" based on strand yet + dojo.addClass(segDiv, "subfeature"); + dojo.addClass(segDiv, CDSclass); + if (Util.is_ie6) segDiv.appendChild(document.createComment()); + segDiv.style.cssText = + "left: " + (100 * ((cdsSegStart - subStart) / subLength)) + "%;" + + "width: " + (100 * ((cdsSegEnd - cdsSegStart) / subLength)) + "%;"; + dojo.addClass(segDiv, "cds-frame" + cdsFrame); + subDiv.appendChild(segDiv); + } + priorCdsLength += (cdsSegEnd - cdsSegStart); + + // make right UTR (if needed) + if (cdsMax < subEnd) { + utrStart = cdsSegEnd; + utrEnd = subEnd; + if (render) { + segDiv = document.createElement("div"); + // not worrying about appending "plus-"/"minus-" based on strand yet + dojo.addClass(segDiv, "subfeature"); + dojo.addClass(segDiv, UTRclass); + if (Util.is_ie6) segDiv.appendChild(document.createComment()); + segDiv.style.cssText = + "left: " + (100 * ((utrStart - subStart) / subLength)) + "%;" + + "width: " + (100 * ((utrEnd - utrStart) / subLength)) + "%;"; + subDiv.appendChild(segDiv); + } + } + } + return priorCdsLength; + }, + + + /* + * selection occurs on mouse down + * mouse-down on unselected feature -- deselect all & select feature + * mouse-down on selected feature -- no change to selection (but may start drag?) + * mouse-down on "empty" area -- deselect all + * (WARNING: this is preferred behavior, but conflicts with dblclick for zoom -- zoom would also deselect) + * therefore have mouse-click on empty area deselect all (no conflict with dblclick) + * shift-mouse-down on unselected feature -- add feature to selection + * shift-mouse-down on selected feature -- remove feature from selection + * shift-mouse-down on "empty" area -- no change to selection + * + * "this" should be a featdiv or subfeatdiv + */ + onFeatureMouseDown: function(event) { + // event.stopPropagation(); + if( this.verbose_selection || this.verbose_drag ) { + console.log("DFT.onFeatureMouseDown called"); + console.log("genome coord: " + this.getGenomeCoord(event)); + } + + // drag_create conditional needed in older strategy using trigger(event) for feature drag bootstrapping with JQuery 1.5, + // but not with with JQuery 1.7+ strategy using _mouseDown(event), since _mouseDown call doesn't lead to onFeatureMouseDown() call + // if (this.drag_create) { this.drag_create = null; return; } + this.handleFeatureSelection(event); + if (this.drag_enabled) { + this.handleFeatureDragSetup(event); + } + }, + + handleFeatureSelection: function( event ) { + var ftrack = this; + var selman = ftrack.selectionManager; + var featdiv = (event.currentTarget || event.srcElement); + var feat = featdiv.feature || featdiv.subfeature; + + if( selman.unselectableTypes[feat.get('type')] ) { + return; + } + + var already_selected = selman.isSelected( { feature: feat, track: ftrack } ); + var parent_selected = false; + var parent = feat.parent(); + if (parent) { + parent_selected = selman.isSelected( { feature: parent, track: ftrack } ); + } + if (this.verbose_selection) { + console.log("DFT.handleFeatureSelection() called, actual mouse event"); + console.log(featdiv); + console.log(feat); + console.log("already selected: " + already_selected + ", parent selected: " + parent_selected + + ", shift: " + (event.shiftKey)); + } + // if parent is selected, allow propagation of event up to parent, + // in order to ensure parent draggable setup and triggering + // otherwise stop propagation + if (! parent_selected) { + event.stopPropagation(); + } + if (event.shiftKey) { + if (already_selected) { // if shift-mouse-down and this already selected, deselect this + selman.removeFromSelection( { feature: feat, track: this }); + } + else if (parent_selected) { + // if shift-mouse-down and parent selected, do nothing -- + // event will get propagated up to parent, where parent will get deselected... + // selman.removeFromSelection(parent); + } + else { // if shift-mouse-down and neither this or parent selected, select this + // children are auto-deselected by selection manager when parent is selected + selman.addToSelection({ feature: feat, track: this }, true); + } + } + else if (event.altKey) { + if (already_selected) { + // select entire feature + selman.addToSelection({ feature: feat.parent(), track: this}, false); + } + else if (parent_selected) { + // do nothing + } + else { + selman.addToSelection({ feature: feat.parent(), track: this}, false); + } + } + else if (event.ctrlKey) { + if (already_selected) { + // do nothing + } + else if (parent_selected) { + // do nothing + } + else { + selman.addToSelection({ feature: feat, track: this}, false); + } + } + else if (event.metaKey) { + } + else { // no shift modifier + if (already_selected) { // if this selected, do nothing (this remains selected) + if (this.verbose_selection) { console.log("already selected"); } + } + else { + if (parent_selected) { + // if this not selected but parent selected, do nothing (parent remains selected) + // event will propagate up (since parent_selected), so draggable check + // will be done in bubbled parent event + } + else { // if this not selected and parent not selected, select this + selman.clearSelection(); + selman.addToSelection({ track: this, feature: feat}); + } + } + } + }, + + /* + * WARNING: assumes one level (featdiv has feature) + * or two-level (featdiv has feature, subdivs have subfeature) feature hierarchy + * attaching ghost to pinned AnnotTrack or SequenceTrack to ensure that stays on top + */ + handleFeatureDragSetup: function(event) { + var ftrack = this; + var featdiv = (event.currentTarget || event.srcElement); + if (this.verbose_drag) { console.log("called handleFeatureDragSetup()"); console.log(featdiv); } + var feat = featdiv.feature || featdiv.subfeature; + var selected = this.selectionManager.isSelected( { feature: feat, track: ftrack }); + // set all other tracks to standard track zIndex, + // set this track to > than others to ensure ghost is drawn on top of all other tracks + /* ftrack.div.style.zIndex = 10; + $(ftrack.gview.tracks).each( function(index, track) { + if (track.div !== ftrack.div && track.div.style.zIndex !== 7) { + track.div.style.zIndex = 7; + } + } ); + */ + + /* + // simple version for testing + // (no multiselect ghosting, no appendTo redirection, no event retriggering for simultaneous select & drag) + if (selected) { + var $featdiv = $(featdiv); + $featdiv.draggable( { + helper: 'clone', + opacity: 0.5, + axis: 'y', + } ); + } + */ + /** + * ideally would only make $.draggable call once for each selected div + * but having problems with draggability disappearing from selected divs + * that $.draggable was already called on + * therefore whenever mousedown on a previously selected div also want to + * check that draggability and redo if missing + */ + if (selected) { + var $featdiv = $(featdiv); + if (! $featdiv.hasClass("ui-draggable")) { + if (this.verbose_drag) { + console.log("setting up dragability"); + console.log(featdiv); + } + var atrack = ftrack.webapollo.getAnnotTrack(); + if (! atrack) { atrack = ftrack.webapollo.getSequenceTrack(); } + var fblock = ftrack.getBlock(featdiv); + + // append drag ghost to featdiv block's equivalent block in annotation track if present, + // else append to equivalent block in sequence track if present, + // else append to featdiv's block + var ablock = ( atrack ? atrack.getEquivalentBlock(fblock) : fblock); + + $featdiv.draggable( // draggable() adds "ui-draggable" class to div + { + zIndex: 200, + appendTo: ablock.domNode, // would default to featdiv's parent div + // custom helper for pseudo-multi-drag ("pseudo" because multidrag is visual only -- + // handling of draggable when dropped is already done through selection) + // strategy for custom helper is to make a "holder" div with same dimensionsas featdiv + // that's (mostly) a clone of the featdiv draggable is being called on + // (since draggable seems to like that), + // then add clones of all selected feature divs (including another clone of featdiv) + // to holder, with dimensions of each clone recalculated as pixels and set relative to + // featdiv that the drag is actually initiated on (and thus relative to the holder's + // dimensions) + + // helper: 'clone', + helper: function() { + // var $featdiv_copy = $featdiv.clone(); + var $pfeatdiv; + // get top-level feature (assumes one or two-level feature hierarchy) + if (featdiv.subfeature) { + $pfeatdiv = $(featdiv.parentNode); + } + else { + $pfeatdiv = $(featdiv); + } + var $holder = $pfeatdiv.clone(); + $holder.removeClass(); + // just want the shell of the top-level feature, so remove children + // (selected children will be added back in below) + $holder.empty(); + $holder.addClass("custom-multifeature-draggable-helper"); + var holder = $holder[0]; + // var featdiv_copy = $featdiv_copy[0]; + + var foffset = $pfeatdiv.offset(); + var fheight = $pfeatdiv.height(); + var fwidth = $pfeatdiv.width(); + var ftop = foffset.top; + var fleft = foffset.left; + if (this.verbose_drag) { + console.log("featdiv dimensions: "); + console.log(foffset); console.log("height: " + fheight + ", width: " + fwidth); + } + var selection = ftrack.selectionManager.getSelection(); + var selength = selection.length; + for (var i=0; i 2 levels deep + var subfeat = featdiv.subfeature; + // if (subfeat && (! unselectableTypes[subfeat.get('type')])) { // only allow double-click parent selection for selectable features + if( subfeat && selman.isSelected({ feature: subfeat, track: ftrack }) ) { // only allow double-click of child for parent selection if child is already selected + var parent = subfeat.parent(); + // select parent feature + // children (including subfeat double-clicked one) are auto-deselected in FeatureSelectionManager if parent is selected + if( parent ) { selman.addToSelection({ feature: parent, track: ftrack }); } + } + }, + + + /** + * returns first feature or subfeature div (including itself) + * found when crawling towards root from branch in + * feature/subfeature/descendants div hierachy + */ + getLowestFeatureDiv: function(elem) { + while (!elem.feature && !elem.subfeature) { + elem = elem.parentNode; + if (elem === document) {return null;} + } + return elem; + }, + + + /** + * Near as I can tell, track.showRange is called every time the + * appearance of the track changes in a way that would cause + * feature divs to be added or deleted (or moved? -- not sure, + * feature moves may also happen elsewhere?) So overriding + * showRange here to try and map selected features to selected + * divs and make sure the divs have selection style set + */ + showRange: function( first, last, startBase, bpPerBlock, scale, + containerStart, containerEnd ) { + this.inherited( arguments ); + + // console.log("called DraggableFeatureTrack.showRange(), block range: " + + // this.firstAttached + "--" + this.lastAttached + ", " + (this.lastAttached - this.firstAttached)); + // redo selection styles for divs in case any divs for selected features were changed/added/deleted + var srecs = this.selectionManager.getSelection(); + for (var sin in srecs) { + // only look for selected features in this track -- + // otherwise will be redoing (sfeats.length * tracks.length) times instead of sfeats.length times, + // because showRange is getting called for each track + var srec = srecs[sin]; + if (srec.track === this) { + // some or all feature divs are usually recreated in a showRange call + // therefore calling track.selectionAdded() to retrigger setting of selected-feature CSS style, etc. on new feat divs + this.selectionAdded(srec); + } + } + }, + + + +/* + * for the input mouse event, returns genome position under mouse IN 1-BASED INTERBASE COORDINATES + * WARNING: returns base position relative to UI coordinate system + * (which is 1-based interbase) + * But for most elements in genome view (features, graphs, etc.) the underlying data structures are + * in 0-base interbase coordinate system + * So if you want data structure coordinates, you need to do (getUiGenomeCoord() - 1) + * or use the convenience function getGenomeCoord() + * + * event can be on GenomeView.elem or any descendant DOM elements (track, block, feature divs, etc.) + * assumes: + * event is a mouse event (plain Javascript event or JQuery event) + * elem is a DOM element OR JQuery wrapped set (in which case result is based on first elem in result set) + * elem is displayed (see JQuery.offset() docs) + * no border/margin/padding set on the doc element (see JQuery.offset() docs) + * if in IE<9, either page is not scrollable (in the HTML page sense) OR event is JQuery event + * (currently JBrowse index.html page is not scrollable (JBrowse internal scrolling is NOT same as HTML page scrolling)) + */ + +/* + getUiGenomeCoord: function(mouseEvent) { + return Math.floor(this.gview.absXtoBp(mouseEvent.pageX)); + }, +*/ + +/** + * for the input mouse event, returns genome position under mouse IN 0-BASED INTERBASE COORDINATES + * WARNING: + * returns genome coord in 0-based interbase (which is how internal data structure represent coords), + * instead of 1-based interbase (which is how UI displays coordinates) + * if need display coordinates, use getUiGenomeCoord() directly instead + * + * otherwise same capability and assumptions as getUiGenomeCoord(): + * event can be on GenomeView.elem or any descendant DOM elements (track, block, feature divs, etc.) + * assumes: + * event is a mouse event (plain Javascript event or JQuery event) + * elem is a DOM element OR JQuery wrapped set (in which case result is based on first elem in result set) + * elem is displayed (see JQuery.offset() docs) + * no border/margin/padding set on the doc element (see JQuery.offset() docs) + * if in IE<9, either page is not scrollable (in the HTML page sense) OR event is JQuery event + * (currently JBrowse index.html page is not scrollable (JBrowse internal scrolling is NOT same as HTML page scrolling)) + * + */ + getGenomeCoord: function(mouseEvent) { + return Math.floor(this.gview.absXtoBp(mouseEvent.pageX)); + // return this.getUiGenomeCoord(mouseEvent) - 1; + }, + + _makeFeatureContextMenu: function( featDiv, menuTemplate ) { + var atrack = this.webapollo.getAnnotTrack(); + + var menu = this.inherited(arguments); + menu.addChild(new dijitMenuSeparator()); + + this.contextMenuItems = {}; + + var createAnnotationMenu = new dijitMenu(); + createAnnotationMenu.addChild(new dijitMenuItem( { + label: "gene", + onClick: dojo.hitch(this, function() { + var selection = this.selectionManager.getSelection(); + this.selectionManager.clearSelection(); + atrack.createAnnotations(selection); + }) + })); + createAnnotationMenu.addChild(new dijitMenuItem( { + label: "pseudogene", + onClick: dojo.hitch(this, function() { + var selection = this.selectionManager.getSelection(); + var selFeats = this.selectionManager.getSelectedFeatures(); + this.selectionManager.clearSelection(); + atrack.createGenericAnnotations(selFeats, "transcript", null, "pseudogene"); + }) + })); + createAnnotationMenu.addChild(new dijitMenuItem( { + label: "tRNA", + onClick: dojo.hitch(this, function() { + var selection = this.selectionManager.getSelection(); + var selFeats = this.selectionManager.getSelectedFeatures(); + this.selectionManager.clearSelection(); + atrack.createGenericAnnotations(selFeats, "tRNA", null, "gene"); + }) + })); + createAnnotationMenu.addChild(new dijitMenuItem( { + label: "snRNA", + onClick: dojo.hitch(this, function() { + var selection = this.selectionManager.getSelection(); + var selFeats = this.selectionManager.getSelectedFeatures(); + this.selectionManager.clearSelection(); + atrack.createGenericAnnotations(selFeats, "snRNA", null, "gene"); + }) + })); + createAnnotationMenu.addChild(new dijitMenuItem( { + label: "snoRNA", + onClick: dojo.hitch(this, function() { + var selection = this.selectionManager.getSelection(); + var selFeats = this.selectionManager.getSelectedFeatures(); + this.selectionManager.clearSelection(); + atrack.createGenericAnnotations(selFeats, "snoRNA", null, "gene"); + }) + })); + createAnnotationMenu.addChild(new dijitMenuItem( { + label: "ncRNA", + onClick: dojo.hitch(this, function() { + var selection = this.selectionManager.getSelection(); + var selFeats = this.selectionManager.getSelectedFeatures(); + this.selectionManager.clearSelection(); + atrack.createGenericAnnotations(selFeats, "ncRNA", null, "gene"); + }) + })); + createAnnotationMenu.addChild(new dijitMenuItem( { + label: "rRNA", + onClick: dojo.hitch(this, function() { + var selection = this.selectionManager.getSelection(); + var selFeats = this.selectionManager.getSelectedFeatures(); + this.selectionManager.clearSelection(); + atrack.createGenericAnnotations(selFeats, "rRNA", null, "gene"); + }) + })); + createAnnotationMenu.addChild(new dijitMenuItem( { + label: "miRNA", + onClick: dojo.hitch(this, function() { + var selection = this.selectionManager.getSelection(); + var selFeats = this.selectionManager.getSelectedFeatures(); + this.selectionManager.clearSelection(); + atrack.createGenericAnnotations(selFeats, "miRNA", null, "gene"); + }) + })); + createAnnotationMenu.addChild(new dijitMenuItem( { + label: "repeat_region", + onClick: dojo.hitch(this, function() { + var selection = this.selectionManager.getSelection(); + var selFeats = this.selectionManager.getSelectedFeatures(); + this.selectionManager.clearSelection(); + atrack.createGenericOneLevelAnnotations(selFeats, "repeat_region", true); + }) + })); + createAnnotationMenu.addChild(new dijitMenuItem( { + label: "transposable_element", + onClick: dojo.hitch(this, function() { + var selection = this.selectionManager.getSelection(); + var selFeats = this.selectionManager.getSelectedFeatures(); + this.selectionManager.clearSelection(); + atrack.createGenericOneLevelAnnotations(selFeats, "transposable_element", true); + }) + })); + + var createAnnotationMenuItem = new dijitPopupMenuItem( { + label: "Create new annotation", + popup: createAnnotationMenu + } ); + this.contextMenuItems["create_annotation"] = createAnnotationMenuItem; + menu.addChild(createAnnotationMenuItem); + + dojo.connect(menu, "onOpen", dojo.hitch(this, function() { + this.updateContextMenu(); + })); + + }, + + // override getLayout to access addRect method + _getLayout: function () { + var thisB = this; + var browser = this.browser; + var layout = this.inherited(arguments); + var clabel = this.name + "-collapsed"; + return declare.safeMixin(layout, { + addRect: function (id, left, right, height, data) { + var cm = thisB.collapsedMode || browser.cookie(clabel) == "true"; + //store height for collapsed mode + if (cm) { + var pHeight = Math.ceil(height / this.pitchY); + this.pTotalHeight = Math.max(this.pTotalHeight || 0, pHeight); + } + var ycoord = (data&&data.get('strand'))==-1?20:0; + return cm ? ycoord : this.inherited(arguments); + } + }); + }, + _trackMenuOptions: function () { + var thisB = this; + var browser = this.browser; + var clabel = this.name + "-collapsed"; + var options = this.inherited(arguments) || []; + + options.push({ + label: "Collapsed view", + title: "Collapsed view", + type: 'dijit/CheckedMenuItem', + checked: !!('collapsedMode' in thisB ? thisB.collapsedMode : browser.cookie(clabel) == "true"), + onClick: function (event) { + thisB.collapsedMode = this.get("checked"); + browser.cookie(clabel, this.get("checked") ? "true" : "false"); + var temp = thisB.showLabels; + if (this.get("checked")) { + thisB.showLabels = false; + } + else if (thisB.previouslyShowLabels) { + thisB.showLabels = true; + } + thisB.previouslyShowLabels = temp; + delete thisB.trackMenu; + thisB.makeTrackMenu(); + thisB.redraw(); + } + }); + + return options; + }, + updateContextMenu: function() { + var atrack = this.webapollo.getAnnotTrack(); + if (!atrack || !atrack.isLoggedIn() || !atrack.hasWritePermission()) { + this.contextMenuItems["create_annotation"].set("disabled", true); + } + else { + this.contextMenuItems["create_annotation"].set("disabled", false); + } + + }, + updateFeatureLabelPositions: function( coords ) { + var showLabels=this.webapollo._showLabels; + if( ! 'x' in coords ) + return; + + array.forEach( this.blocks, function( block, blockIndex ) { + + + // calculate the view left coord relative to the + // block left coord in units of pct of the block + // width + if( ! block || ! this.label ) + return; + var viewLeft = 100 * ( (this.label.offsetLeft+(showLabels?this.label.offsetWidth:0)) - block.domNode.offsetLeft ) / block.domNode.offsetWidth + 2; + + // if the view start is unknown, or is to the + // left of this block, we don't have to worry + // about adjusting the feature labels + if( ! viewLeft ) + return; + + var blockWidth = block.endBase - block.startBase; + + array.forEach( block.domNode.childNodes, function( featDiv ) { + if( ! featDiv.label ) return; + var labelDiv = featDiv.label; + var feature = featDiv.feature; + + // get the feature start and end in terms of block width pct + var minLeft = parseInt( feature.get('start') ); + minLeft = 100 * (minLeft - block.startBase) / blockWidth; + var maxLeft = parseInt( feature.get('end') ); + maxLeft = 100 * ( (maxLeft - block.startBase) / blockWidth + - labelDiv.offsetWidth / block.domNode.offsetWidth + ); + + // move our label div to the view start if the start is between the feature start and end + labelDiv.style.left = Math.max( minLeft, Math.min( viewLeft, maxLeft ) ) + '%'; + + },this); + },this); + } + +}); + + return draggableTrack; +}); + +/* Subclass of FeatureTrack that allows features to be selected, + and dragged and dropped into the annotation track to create annotations. + + Note: + for selection to work for features that cross block boundaries, z-index of feature style MUST be set, and must be > 0 + otherwise what happens is: + feature div inherits z-order from parent, so same z-order as block + so feature div pixels may extend into next block, but next block draws ON TOP OF IT (assuming next block added + to parent after current block). So events over part of feature div that isn't within it's parent block will never + reach feature div but instead be triggered on next block + This issue will be more obvious if blocks have background color set since then not only will selection not work but + part of feature div that extends into next block won't even be visible, since next block background will render over it + */ + + + + + /* + Copyright (c) 2010-2011 Berkeley Bioinformatics Open-source Projects & Lawrence Berkeley National Labs + + This package and its accompanying libraries are free software; you can + redistribute it and/or modify it under the terms of the LGPL (either + version 2.1, or at your option, any later version) or the Artistic + License 2.0. Refer to LICENSE for the full license text. +*/ diff --git a/client/apollo/js/View/Track/HTMLFeatures.js b/client/apollo/js/View/Track/HTMLFeatures.js index 51c97c511f..5538b2662d 100644 --- a/client/apollo/js/View/Track/HTMLFeatures.js +++ b/client/apollo/js/View/Track/HTMLFeatures.js @@ -50,6 +50,7 @@ define( [ //featureStart and featureEnd indicate how far left or right //the feature extends in bp space, including labels //and arrowheads if applicable + console.log('AAAAAAA'); var refSeqName = this.refSeq.name ; feature = ProjectionUtils.projectJSONFeature(feature,refSeqName); From 491b243fb68a5c721de63aa6409830f2045851df Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Thu, 14 Dec 2017 07:11:07 -0800 Subject: [PATCH 07/21] fixed missing include --- client/apollo/js/Store/SeqFeature/GFF3Tabix.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js index e3fe5941f0..93851b0635 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js +++ b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js @@ -3,6 +3,7 @@ define([ 'dojo/_base/lang', 'dojo/_base/array', 'dojo/Deferred', + 'dojo/query', 'JBrowse/Model/XHRBlob', 'WebApollo/Store/TabixIndexedFile', 'WebApollo/Store/SeqFeature/GlobalStatsEstimationMixin', @@ -16,6 +17,7 @@ define([ lang, array, Deferred, + query, XHRBlob, TabixIndexedFile, GlobalStatsEstimationMixin, From 9da0019d075966f38f3c0d3f4d0c344563e42804 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Fri, 15 Dec 2017 10:52:15 -0800 Subject: [PATCH 08/21] added logging and over-rode some subfeature methods --- .../apollo/js/Store/SeqFeature/GFF3Tabix.js | 11 ++- .../js/View/Track/DraggableHTMLFeatures.js | 2 +- .../Track/DraggableProjectedHTMLFeatures.js | 82 ++++++++++++++++++- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js index 93851b0635..97d2f669d8 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js +++ b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js @@ -127,11 +127,14 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], if (reverse) { line.fields[6] = ProjectionUtils.flipStrand(line.fields[6]); } - // console.log('- VS -'); - // console.log(line); } - - parser._buffer_feature( thisB.lineToFeature(line) ); + var bufferFeature = thisB.lineToFeature(line); + if(line.fields.join('').indexOf('GB53498-RA')>=0){ + console.log(' -> '); + console.log(line); + console.log(bufferFeature); + } + parser._buffer_feature( bufferFeature ); }, function() { parser.finish(); diff --git a/client/apollo/js/View/Track/DraggableHTMLFeatures.js b/client/apollo/js/View/Track/DraggableHTMLFeatures.js index e418e74dc2..4a6c81653a 100644 --- a/client/apollo/js/View/Track/DraggableHTMLFeatures.js +++ b/client/apollo/js/View/Track/DraggableHTMLFeatures.js @@ -729,7 +729,7 @@ var draggableTrack = declare( HTMLFeatureTrack, displayStart, displayEnd, block ) { var subfeatdiv = this.inherited( arguments ); - console.log('rendering subfeature') + console.log('rendering subfeature, DraggableHTMLFeatures') if (subfeatdiv) { // just in case subFeatDiv doesn't actually get created var $subfeatdiv = $(subfeatdiv); // adding pointer to track for each subfeatdiv diff --git a/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js b/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js index 471c9b78ff..25ef23dc58 100644 --- a/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js +++ b/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js @@ -728,8 +728,82 @@ var draggableTrack = declare( HTMLFeatureTrack, renderSubfeature: function( feature, featDiv, subfeature, displayStart, displayEnd, block ) { - var subfeatdiv = this.inherited( arguments ); - console.log('rendering subfeature') + console.log('rendering subfeature: DraggableProjectedHTMLFeatures '); + console.log(arguments); + console.log(feature); + // var subfeatdiv = this.inherited( arguments ); + + /** + * START pulling from JBRowse / HTML + */ + var subStart = subfeature.get('start'); + var subEnd = subfeature.get('end'); + var featLength = displayEnd - displayStart; + var type = subfeature.get('type'); + var className; + if( this.config.style.subfeatureClasses ) { + className = this.config.style.subfeatureClasses[type]; + // if no class mapping specified for type, default to subfeature.get('type') + if (className === undefined) { className = type; } + // if subfeatureClasses specifies that subfeature type explicitly maps to null className + // then don't render the feature + else if (className === null) { + return null; + } + } + else { + // if no config.style.subfeatureClasses to specify subfeature class mapping, default to subfeature.get('type') + className = type; + } + + // a className of 'hidden' causes things to not even be rendered + if( className == 'hidden' ) + return null; + + var subDiv = document.createElement("div"); + // used by boolean tracks to do positiocning + subDiv.subfeatureEdges = { s: subStart, e: subEnd }; + + dojo.addClass(subDiv, "subfeature"); + // check for className to avoid adding "null", "plus-null", "minus-null" + if (className) { + switch ( subfeature.get('strand') ) { + case 1: + case '+': + dojo.addClass(subDiv, "plus-" + className); break; + case -1: + case '-': + dojo.addClass(subDiv, "minus-" + className); break; + default: + dojo.addClass(subDiv, className); + } + } + + // if the feature has been truncated to where it doesn't cover + // this subfeature anymore, just skip this subfeature + + var truncate = false; + if (typeof this.config.truncateFeatures !== 'undefined' && this.config.truncateFeatures===true ) + truncate = true; + + if ( truncate && (subEnd <= displayStart || subStart >= displayEnd) ) + return null; + + subDiv.style.cssText = "left: " + (100 * ((subStart - displayStart) / featLength)) + "%;" + + "width: " + (100 * ((subEnd - subStart) / featLength)) + "%;"; + featDiv.appendChild(subDiv); + + block.featureNodes[ subfeature.id() ] = subDiv; + + + var subfeatdiv = subDiv; + /** + * END pull from JBRowse HTML + */ + + console.log(subfeatdiv); + + console.log('renderd inherited subfeature: DraggableProjectedHTMLFeatures '); if (subfeatdiv) { // just in case subFeatDiv doesn't actually get created var $subfeatdiv = $(subfeatdiv); // adding pointer to track for each subfeatdiv @@ -889,7 +963,10 @@ var draggableTrack = declare( HTMLFeatureTrack, handleSubFeatures: function( feature, featDiv, displayStart, displayEnd, block ) { + console.log(feature) var subfeats = feature.get('subfeatures'); + console.log('Draggable Projected HTMLFEatuers, hadnling subfeats: '); + console.log(subfeats) if (! subfeats) { return; } if (! feature.normalized ) { @@ -904,6 +981,7 @@ var draggableTrack = declare( HTMLFeatureTrack, var subfeat; var subtype; + if (wholeCDS) { var cdsStart = wholeCDS.get('start'); var cdsEnd = wholeCDS.get('end'); From 5b76367617de5c34a60d16d3d3fe420a87f2c8b0 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Fri, 15 Dec 2017 14:26:20 -0800 Subject: [PATCH 09/21] canvas is projecting back and forth --- client/apollo/js/ProjectionUtils.js | 13 +- .../apollo/js/Store/SeqFeature/GFF3/Parser.js | 1 - .../apollo/js/Store/SeqFeature/GFF3Tabix.js | 29 +-- .../js/View/Track/DraggableHTMLFeatures.js | 3 - .../js/View/Track/WebApolloCanvasFeatures.js | 165 +++++++++++++++++- 5 files changed, 182 insertions(+), 29 deletions(-) diff --git a/client/apollo/js/ProjectionUtils.js b/client/apollo/js/ProjectionUtils.js index c56e955af9..a7b382406f 100644 --- a/client/apollo/js/ProjectionUtils.js +++ b/client/apollo/js/ProjectionUtils.js @@ -141,8 +141,10 @@ define([ 'dojo/_base/declare', if(!feature.isProjected) return feature ; feature.data.start = feature.data._original_start ; feature.data.end = feature.data._original_end ; + feature.data.strand = feature.data._original_strand; delete feature.data._original_start; delete feature.data._original_end ; + delete feature.data._original_strand ; feature.isProjected = false; if (feature.data.subfeatures) { for (var i = 0; i < feature.data.subfeatures.length; i++) { @@ -156,18 +158,27 @@ define([ 'dojo/_base/declare', * Project a given JSON feature */ ProjectionUtils.projectJSONFeature = function( feature, refSeqName ) { - if(feature.isProjected) return feature ; + if(feature.isProjected) { + consoel.log('already projected'); + return feature ; + } var start = feature.get("start"); var end = feature.get("end"); + var strand = feature.get("strand"); var projectedArray = this.projectCoordinates(refSeqName, start, end); if (!feature.data) { feature.data = {}; } feature.data._original_start = start; feature.data._original_end = end; + feature.data._original_strand = strand; + + feature.data.start = projectedArray[0]; feature.data.end = projectedArray[1]; + // feature.data.strand = this.flipStrand(feature.data._original_strand); + feature.data.strand = this.projectStrand(refSeqName,feature.data.strand); feature.isProjected = true; if (feature.data.subfeatures) { for (var i = 0; i < feature.data.subfeatures.length; i++) { diff --git a/client/apollo/js/Store/SeqFeature/GFF3/Parser.js b/client/apollo/js/Store/SeqFeature/GFF3/Parser.js index f291a572a1..b8061d8399 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3/Parser.js +++ b/client/apollo/js/Store/SeqFeature/GFF3/Parser.js @@ -18,7 +18,6 @@ define([ return declare( [Parser], { constructor: function( args ) { - console.log('using the Apollo parser 2'); lang.mixin( this, { featureCallback: args.featureCallback || function() {}, endCallback: args.endCallback || function() {}, diff --git a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js index 97d2f669d8..56bb7a3a49 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js +++ b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js @@ -100,12 +100,15 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], var sequenceListObject = ProjectionUtils.parseSequenceList(refSeqName); chrName = sequenceListObject[0].name ; reverse = sequenceListObject[0].reverse; + var unprojectedArray = ProjectionUtils.unProjectCoordinates(refSeqName,query.start,query.end); + min = unprojectedArray[0]; + max = unprojectedArray[1]; } else{ + min = query.start ; + max = query.end ; chrName = query.ref ; } - min = query.start ; - max = query.end ; // console.log(min + ' ' + max); @@ -114,27 +117,7 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], min, max, function( line ) { - if(ProjectionUtils.isSequenceList(refSeqName)) { - // console.log(refSeqName); - // console.log(line); - var unprojectedArray = ProjectionUtils.unProjectCoordinates(refSeqName, line.start, line.end); - min = unprojectedArray[0]; - max = unprojectedArray[1]; - line.start = min; - line.end = max; - line.fields[3] = min; - line.fields[4] = max; - if (reverse) { - line.fields[6] = ProjectionUtils.flipStrand(line.fields[6]); - } - } - var bufferFeature = thisB.lineToFeature(line); - if(line.fields.join('').indexOf('GB53498-RA')>=0){ - console.log(' -> '); - console.log(line); - console.log(bufferFeature); - } - parser._buffer_feature( bufferFeature ); + parser._buffer_feature( thisB.lineToFeature(line)); }, function() { parser.finish(); diff --git a/client/apollo/js/View/Track/DraggableHTMLFeatures.js b/client/apollo/js/View/Track/DraggableHTMLFeatures.js index 4a6c81653a..f522398c76 100644 --- a/client/apollo/js/View/Track/DraggableHTMLFeatures.js +++ b/client/apollo/js/View/Track/DraggableHTMLFeatures.js @@ -692,9 +692,7 @@ var draggableTrack = declare( HTMLFeatureTrack, */ renderFeature: function(feature, uniqueId, block, scale, labelScale, descriptionScale, containerStart, containerEnd, rclass, clsName ) { - console.log('BBBBBB 1'); var featdiv = this.inherited( arguments ); - console.log('BBBBBB 2'); if( featdiv ) { // just in case featDiv doesn't actually get created var $featdiv = $(featdiv); @@ -729,7 +727,6 @@ var draggableTrack = declare( HTMLFeatureTrack, displayStart, displayEnd, block ) { var subfeatdiv = this.inherited( arguments ); - console.log('rendering subfeature, DraggableHTMLFeatures') if (subfeatdiv) { // just in case subFeatDiv doesn't actually get created var $subfeatdiv = $(subfeatdiv); // adding pointer to track for each subfeatdiv diff --git a/client/apollo/js/View/Track/WebApolloCanvasFeatures.js b/client/apollo/js/View/Track/WebApolloCanvasFeatures.js index eeda0a1fbe..ece4b24c17 100644 --- a/client/apollo/js/View/Track/WebApolloCanvasFeatures.js +++ b/client/apollo/js/View/Track/WebApolloCanvasFeatures.js @@ -1,6 +1,8 @@ define( [ 'dojo/_base/declare', 'dojo/_base/array', + 'dojo/dom-construct', + 'dojo/Deferred', 'JBrowse/View/Track/CanvasFeatures', 'dijit/Menu', 'dijit/MenuItem', @@ -11,10 +13,13 @@ define( [ 'JBrowse/Util', 'JBrowse/Model/SimpleFeature', 'WebApollo/View/Projection/FASTA', + 'WebApollo/ProjectionUtils', 'WebApollo/SequenceOntologyUtils' ], function( declare, array, + domConstruct, + Deferred, CanvasFeaturesTrack, dijitMenu, dijitMenuItem, @@ -25,6 +30,7 @@ define( [ Util, SimpleFeature, FASTAView, + ProjectionUtils, SeqOnto ) { @@ -37,7 +43,6 @@ return declare( CanvasFeaturesTrack, })); }, _defaultConfig: function() { - console.log("WA config",document.body); var config = Util.deepUpdate(dojo.clone(this.inherited(arguments)), { style: { @@ -127,6 +132,164 @@ return declare( CanvasFeaturesTrack, return config; }, + fillFeatures: function( args ) { + var thisB = this; + + var blockIndex = args.blockIndex; + var block = args.block; + var blockWidthPx = block.domNode.offsetWidth; + var scale = args.scale; + var leftBase = args.leftBase; + var rightBase = args.rightBase; + var finishCallback = args.finishCallback; + + var fRects = []; + + // count of how many features are queued up to be laid out + var featuresInProgress = 0; + // promise that resolved when all the features have gotten laid out by their glyphs + var featuresLaidOut = new Deferred(); + // flag that tells when all features have been read from the + // store (not necessarily laid out yet) + var allFeaturesRead = false; + + var errorCallback = dojo.hitch( thisB, function( e ) { + this._handleError( e, args ); + finishCallback(e); + }); + + var layout = this._getLayout( scale ); + + // query for a slightly larger region than the block, so that + // we can draw any pieces of glyphs that overlap this block, + // but the feature of which does not actually lie in the block + // (long labels that extend outside the feature's bounds, for + // example) + var bpExpansion = Math.round( this.config.maxFeatureGlyphExpansion / scale ); + + var refSeqName = this.refSeq.name ; + + var region = { ref: refSeqName, + start: Math.max( 0, leftBase - bpExpansion ), + end: rightBase + bpExpansion + }; + + this.store.getFeatures( region, + function( feature ) { + + + if( thisB.destroyed || ! thisB.filterFeature( feature ) ) + return; + fRects.push( null ); // put a placeholder in the fRects array + featuresInProgress++; + var rectNumber = fRects.length-1; + + feature = ProjectionUtils.projectJSONFeature(feature,refSeqName); + + // get the appropriate glyph object to render this feature + thisB.getGlyph( + args, + feature, + function( glyph ) { + // have the glyph attempt + // to add a rendering of + // this feature to the + // layout + var fRect = glyph.layoutFeature( + args, + layout, + feature + ); + if( fRect === null ) { + // could not lay out, would exceed our configured maxHeight + // mark the block as exceeding the max height + block.maxHeightExceeded = true; + } + else { + // laid out successfully + if( !( fRect.l >= blockWidthPx || fRect.l+fRect.w < 0 ) ) + fRects[rectNumber] = fRect; + } + + // this might happen after all the features have been sent from the store + if( ! --featuresInProgress && allFeaturesRead ) { + featuresLaidOut.resolve(); + } + }, + errorCallback + ); + }, + + // callback when all features sent + function () { + if( thisB.destroyed ) + return; + + allFeaturesRead = true; + if( ! featuresInProgress && ! featuresLaidOut.isFulfilled() ) { + featuresLaidOut.resolve(); + } + + featuresLaidOut.then( function() { + + var totalHeight = layout.getTotalHeight(); + var c = block.featureCanvas = + domConstruct.create( + 'canvas', + { height: totalHeight, + width: block.domNode.offsetWidth+1, + style: { + cursor: 'default', + height: totalHeight+'px', + position: 'absolute' + }, + innerHTML: 'Your web browser cannot display this type of track.', + className: 'canvas-track' + }, + block.domNode + ); + var ctx = c.getContext('2d'); + + // finally query the various pixel ratios + var ratio = Util.getResolution( ctx, thisB.browser.config.highResolutionMode ); + // upscale canvas if the two ratios don't match + if ( thisB.browser.config.highResolutionMode != 'disabled' && ratio >= 1 ) { + + var oldWidth = c.width; + var oldHeight = c.height; + + c.width = oldWidth * ratio; + c.height = oldHeight * ratio; + + c.style.width = oldWidth + 'px'; + c.style.height = oldHeight + 'px'; + + // now scale the context to counter + // the fact that we've manually scaled + // our canvas element + ctx.scale(ratio, ratio); + } + + + + if( block.maxHeightExceeded ) + thisB.markBlockHeightOverflow( block ); + + thisB.heightUpdate( totalHeight, + blockIndex ); + + + thisB.renderFeatures( args, fRects ); + + thisB.renderClickMap( args, fRects ); + + finishCallback(); + }); + }, + errorCallback + ); + }, + _renderUnderlyingReferenceSequence: function( track, f, featDiv, container ) { // render the sequence underlying this feature if possible var field_container = dojo.create('div', { className: 'field_container feature_sequence' }, container ); From bd717dc60c27842a7c7c332717cf59f84e22a5e9 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Fri, 15 Dec 2017 14:43:28 -0800 Subject: [PATCH 10/21] fixed bade include --- client/apollo/js/ProjectionUtils.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/apollo/js/ProjectionUtils.js b/client/apollo/js/ProjectionUtils.js index a7b382406f..417bec6e79 100644 --- a/client/apollo/js/ProjectionUtils.js +++ b/client/apollo/js/ProjectionUtils.js @@ -158,10 +158,7 @@ define([ 'dojo/_base/declare', * Project a given JSON feature */ ProjectionUtils.projectJSONFeature = function( feature, refSeqName ) { - if(feature.isProjected) { - consoel.log('already projected'); - return feature ; - } + if(feature.isProjected) return feature ; var start = feature.get("start"); var end = feature.get("end"); @@ -177,7 +174,6 @@ define([ 'dojo/_base/declare', feature.data.start = projectedArray[0]; feature.data.end = projectedArray[1]; - // feature.data.strand = this.flipStrand(feature.data._original_strand); feature.data.strand = this.projectStrand(refSeqName,feature.data.strand); feature.isProjected = true; if (feature.data.subfeatures) { From 1398932bb791b18b7f571aaac101294ae1ae0fe9 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Fri, 15 Dec 2017 17:16:53 -0800 Subject: [PATCH 11/21] got subfeatures to show up in some instances , removed logs --- .../Track/DraggableProjectedHTMLFeatures.js | 157 +++++++++--------- client/apollo/js/View/Track/HTMLFeatures.js | 1 - 2 files changed, 77 insertions(+), 81 deletions(-) diff --git a/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js b/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js index 25ef23dc58..16ef80df96 100644 --- a/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js +++ b/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js @@ -692,9 +692,7 @@ var draggableTrack = declare( HTMLFeatureTrack, */ renderFeature: function(feature, uniqueId, block, scale, labelScale, descriptionScale, containerStart, containerEnd, rclass, clsName ) { - console.log('CCCC 1'); var featdiv = this.inherited( arguments ); - console.log('CCCCC 2'); if( featdiv ) { // just in case featDiv doesn't actually get created var $featdiv = $(featdiv); @@ -728,82 +726,78 @@ var draggableTrack = declare( HTMLFeatureTrack, renderSubfeature: function( feature, featDiv, subfeature, displayStart, displayEnd, block ) { - console.log('rendering subfeature: DraggableProjectedHTMLFeatures '); - console.log(arguments); - console.log(feature); - // var subfeatdiv = this.inherited( arguments ); - - /** - * START pulling from JBRowse / HTML - */ - var subStart = subfeature.get('start'); - var subEnd = subfeature.get('end'); - var featLength = displayEnd - displayStart; - var type = subfeature.get('type'); - var className; - if( this.config.style.subfeatureClasses ) { - className = this.config.style.subfeatureClasses[type]; - // if no class mapping specified for type, default to subfeature.get('type') - if (className === undefined) { className = type; } - // if subfeatureClasses specifies that subfeature type explicitly maps to null className - // then don't render the feature - else if (className === null) { - return null; - } - } - else { - // if no config.style.subfeatureClasses to specify subfeature class mapping, default to subfeature.get('type') - className = type; - } - - // a className of 'hidden' causes things to not even be rendered - if( className == 'hidden' ) - return null; - - var subDiv = document.createElement("div"); - // used by boolean tracks to do positiocning - subDiv.subfeatureEdges = { s: subStart, e: subEnd }; - - dojo.addClass(subDiv, "subfeature"); - // check for className to avoid adding "null", "plus-null", "minus-null" - if (className) { - switch ( subfeature.get('strand') ) { - case 1: - case '+': - dojo.addClass(subDiv, "plus-" + className); break; - case -1: - case '-': - dojo.addClass(subDiv, "minus-" + className); break; - default: - dojo.addClass(subDiv, className); - } - } - - // if the feature has been truncated to where it doesn't cover - // this subfeature anymore, just skip this subfeature - - var truncate = false; - if (typeof this.config.truncateFeatures !== 'undefined' && this.config.truncateFeatures===true ) - truncate = true; - - if ( truncate && (subEnd <= displayStart || subStart >= displayEnd) ) - return null; - - subDiv.style.cssText = "left: " + (100 * ((subStart - displayStart) / featLength)) + "%;" - + "width: " + (100 * ((subEnd - subStart) / featLength)) + "%;"; - featDiv.appendChild(subDiv); - - block.featureNodes[ subfeature.id() ] = subDiv; - - - var subfeatdiv = subDiv; - /** - * END pull from JBRowse HTML - */ - - console.log(subfeatdiv); + var subfeatdiv = this.inherited( arguments ); + // /** + // * START pulling from JBRowse / HTML + // */ + // var subStart = subfeature.get('start'); + // var subEnd = subfeature.get('end'); + // var featLength = displayEnd - displayStart; + // var type = subfeature.get('type'); + // var className; + // if( this.config.style.subfeatureClasses ) { + // className = this.config.style.subfeatureClasses[type]; + // // if no class mapping specified for type, default to subfeature.get('type') + // if (className === undefined) { className = type; } + // // if subfeatureClasses specifies that subfeature type explicitly maps to null className + // // then don't render the feature + // else if (className === null) { + // return null; + // } + // } + // else { + // // if no config.style.subfeatureClasses to specify subfeature class mapping, default to subfeature.get('type') + // className = type; + // } + // + // // a className of 'hidden' causes things to not even be rendered + // if( className == 'hidden' ) + // return null; + // + // var subDiv = document.createElement("div"); + // // used by boolean tracks to do positiocning + // subDiv.subfeatureEdges = { s: subStart, e: subEnd }; + // + // dojo.addClass(subDiv, "subfeature"); + // // check for className to avoid adding "null", "plus-null", "minus-null" + // if (className) { + // switch ( subfeature.get('strand') ) { + // case 1: + // case '+': + // dojo.addClass(subDiv, "plus-" + className); break; + // case -1: + // case '-': + // dojo.addClass(subDiv, "minus-" + className); break; + // default: + // dojo.addClass(subDiv, className); + // } + // } + // + // // if the feature has been truncated to where it doesn't cover + // // this subfeature anymore, just skip this subfeature + // + // var truncate = false; + // if (typeof this.config.truncateFeatures !== 'undefined' && this.config.truncateFeatures===true ) + // truncate = true; + // + // if ( truncate && (subEnd <= displayStart || subStart >= displayEnd) ) + // return null; + // + // subDiv.style.cssText = "left: " + (100 * ((subStart - displayStart) / featLength)) + "%;" + // + "width: " + (100 * ((subEnd - subStart) / featLength)) + "%;"; + // featDiv.appendChild(subDiv); + // + // block.featureNodes[ subfeature.id() ] = subDiv; + // + // + // var subfeatdiv = subDiv; + // /** + // * END pull from JBRowse HTML + // */ + // + // console.log(subfeatdiv); - console.log('renderd inherited subfeature: DraggableProjectedHTMLFeatures '); + // console.log('renderd inherited subfeature: DraggableProjectedHTMLFeatures '); if (subfeatdiv) { // just in case subFeatDiv doesn't actually get created var $subfeatdiv = $(subfeatdiv); // adding pointer to track for each subfeatdiv @@ -963,10 +957,7 @@ var draggableTrack = declare( HTMLFeatureTrack, handleSubFeatures: function( feature, featDiv, displayStart, displayEnd, block ) { - console.log(feature) var subfeats = feature.get('subfeatures'); - console.log('Draggable Projected HTMLFEatuers, hadnling subfeats: '); - console.log(subfeats) if (! subfeats) { return; } if (! feature.normalized ) { @@ -1011,6 +1002,8 @@ var draggableTrack = declare( HTMLFeatureTrack, } var uid = this.getId(subfeat); subtype = subfeat.get('type'); + console.log('subtype: '+subtype) + // don't render "wholeCDS" type // although if subfeatureClases is properly set up, wholeCDS would also be filtered out in renderFeature? // if (subtype == "wholeCDS") { continue; } @@ -1027,10 +1020,14 @@ var draggableTrack = declare( HTMLFeatureTrack, if (subtype === "exon") { // pass even if subDiv is null (not drawn), in order to correctly calc downstream CDS frame priorCdsLength = this.renderExonSegments(subfeat, subDiv, cdsMin, cdsMax, displayStart, displayEnd, priorCdsLength, reverse); } + if(subtype === 'mRNA'){ + this.handleSubFeatures(subfeat,subDiv,displayStart,displayEnd,block); + } if (this.verbose_render) { console.log("in DraggableFeatureTrack.handleSubFeatures, subDiv: "); console.log(subDiv); } + } }, diff --git a/client/apollo/js/View/Track/HTMLFeatures.js b/client/apollo/js/View/Track/HTMLFeatures.js index 5538b2662d..51c97c511f 100644 --- a/client/apollo/js/View/Track/HTMLFeatures.js +++ b/client/apollo/js/View/Track/HTMLFeatures.js @@ -50,7 +50,6 @@ define( [ //featureStart and featureEnd indicate how far left or right //the feature extends in bp space, including labels //and arrowheads if applicable - console.log('AAAAAAA'); var refSeqName = this.refSeq.name ; feature = ProjectionUtils.projectJSONFeature(feature,refSeqName); From 64af0da2b3438609905890559867933aca60fb2a Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Sat, 16 Dec 2017 13:46:55 -0800 Subject: [PATCH 12/21] added logging --- .../View/Track/DraggableProjectedHTMLFeatures.js | 14 +++++++++++++- client/apollo/js/View/Track/HTMLFeatures.js | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js b/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js index 16ef80df96..e7a90ee350 100644 --- a/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js +++ b/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js @@ -46,6 +46,7 @@ var draggableTrack = declare( HTMLFeatureTrack, { // so is dragging dragging: false, + verbose_render: true, _defaultConfig: function() { return Util.deepUpdate( @@ -993,6 +994,13 @@ var draggableTrack = declare( HTMLFeatureTrack, /* WARNING: currently assuming children are ordered by ascending min * (so if on minus strand then need to calc frame starting with the last exon) */ + + if(feature._uniqueID.indexOf('GB53497')==0){ + console.log('type: '+feature.get('type')); + console.log(feature); + console.log('slength: '+slength); + } + for (var i = 0; i < slength; i++) { if (reverse) { subfeat = subfeats[slength-i-1]; @@ -1002,7 +1010,10 @@ var draggableTrack = declare( HTMLFeatureTrack, } var uid = this.getId(subfeat); subtype = subfeat.get('type'); - console.log('subtype: '+subtype) + if(feature._uniqueID.indexOf('GB53497')==0) { + console.log('subtype: ' + subtype) + console.log(subfeat); + } // don't render "wholeCDS" type // although if subfeatureClases is properly set up, wholeCDS would also be filtered out in renderFeature? @@ -1021,6 +1032,7 @@ var draggableTrack = declare( HTMLFeatureTrack, priorCdsLength = this.renderExonSegments(subfeat, subDiv, cdsMin, cdsMax, displayStart, displayEnd, priorCdsLength, reverse); } if(subtype === 'mRNA'){ + console.log('handling the subfeature from '+feature._uniqueID + ' into '+subfeat._uniqueID); this.handleSubFeatures(subfeat,subDiv,displayStart,displayEnd,block); } if (this.verbose_render) { diff --git a/client/apollo/js/View/Track/HTMLFeatures.js b/client/apollo/js/View/Track/HTMLFeatures.js index 51c97c511f..157ec6107e 100644 --- a/client/apollo/js/View/Track/HTMLFeatures.js +++ b/client/apollo/js/View/Track/HTMLFeatures.js @@ -51,6 +51,9 @@ define( [ //the feature extends in bp space, including labels //and arrowheads if applicable + console.log('rendering a feature in HTMLFeatures'); + console.log(feature); + var refSeqName = this.refSeq.name ; feature = ProjectionUtils.projectJSONFeature(feature,refSeqName); From 2d6b419c66b9c0a4de56bacf287533815799f23c Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Mon, 18 Dec 2017 12:52:25 -0800 Subject: [PATCH 13/21] added some baseline SVG code --- client/apollo/js/SVGFeatures.js | 9 +- .../js/View/FeatureGlyph/SVG/Alignment.js | 197 ++++++++++++++ client/apollo/js/View/FeatureGlyph/SVG/Box.js | 166 ++++++++++++ .../js/View/FeatureGlyph/SVG/Diamond.js | 79 ++++++ .../apollo/js/View/FeatureGlyph/SVG/Gene.js | 152 +++++++++++ .../FeatureGlyph/SVG/ProcessedTranscript.js | 252 ++++++++++++++++++ .../js/View/FeatureGlyph/SVG/Segments.js | 124 +++++++++ .../FeatureGlyph/SVG/_FeatureLabelMixin.js | 122 +++++++++ client/apollo/js/View/SVGFeatureGlyph.js | 59 ++++ 9 files changed, 1158 insertions(+), 2 deletions(-) create mode 100644 client/apollo/js/View/FeatureGlyph/SVG/Alignment.js create mode 100644 client/apollo/js/View/FeatureGlyph/SVG/Box.js create mode 100644 client/apollo/js/View/FeatureGlyph/SVG/Diamond.js create mode 100644 client/apollo/js/View/FeatureGlyph/SVG/Gene.js create mode 100644 client/apollo/js/View/FeatureGlyph/SVG/ProcessedTranscript.js create mode 100644 client/apollo/js/View/FeatureGlyph/SVG/Segments.js create mode 100644 client/apollo/js/View/FeatureGlyph/SVG/_FeatureLabelMixin.js create mode 100644 client/apollo/js/View/SVGFeatureGlyph.js diff --git a/client/apollo/js/SVGFeatures.js b/client/apollo/js/SVGFeatures.js index ec6050c279..72cb14c2c0 100644 --- a/client/apollo/js/SVGFeatures.js +++ b/client/apollo/js/SVGFeatures.js @@ -21,6 +21,7 @@ define( [ 'JBrowse/View/Track/_FeatureContextMenusMixin', 'JBrowse/View/Track/_YScaleMixin', 'JBrowse/Model/Location', + 'WebApollo/ProjectionUtils', 'JBrowse/Model/SimpleFeature' ], function( @@ -42,6 +43,7 @@ define( [ FeatureContextMenuMixin, YScaleMixin, Location, + ProjectionUtils, SimpleFeature ) { @@ -376,7 +378,7 @@ return declare( return this.svgHeight; }, guessGlyphType: function(feature) { - return 'JBrowse/View/FeatureGlyph/'+( {'gene': 'Gene', 'mRNA': 'ProcessedTranscript', 'transcript': 'ProcessedTranscript' }[feature.get('type')] || 'Box' ); + return 'WebApollo/View/FeatureGlyph/SVG/'+( {'gene': 'Gene', 'mRNA': 'ProcessedTranscript', 'transcript': 'ProcessedTranscript' }[feature.get('type')] || 'Box' ); }, fillBlock: function( args ) { @@ -717,8 +719,9 @@ return declare( // (long labels that extend outside the feature's bounds, for // example) var bpExpansion = Math.round( this.config.maxFeatureGlyphExpansion / scale ); + var refSeqName = this.refSeq.name ; - var region = { ref: this.refSeq.name, + var region = { ref: refSeqName, start: Math.max( 0, leftBase - bpExpansion ), end: rightBase + bpExpansion }; @@ -731,6 +734,8 @@ return declare( featuresInProgress++; var rectNumber = fRects.length-1; + feature = ProjectionUtils.projectJSONFeature(feature,refSeqName); + // get the appropriate glyph object to render this feature thisB.getGlyph( args, diff --git a/client/apollo/js/View/FeatureGlyph/SVG/Alignment.js b/client/apollo/js/View/FeatureGlyph/SVG/Alignment.js new file mode 100644 index 0000000000..bb24ffb0a7 --- /dev/null +++ b/client/apollo/js/View/FeatureGlyph/SVG/Alignment.js @@ -0,0 +1,197 @@ +define([ + 'dojo/_base/declare', + 'dojo/_base/array', + 'JBrowse/View/FeatureGlyph/Box', + 'JBrowse/Store/SeqFeature/_MismatchesMixin' + ], + function( + declare, + array, + BoxGlyph, + MismatchesMixin + ) { + +return declare( [BoxGlyph,MismatchesMixin], { + + constructor: function() { + + // if showMismatches is false, stub out this object's + // _drawMismatches to be a no-op + if( ! this.config.style.showMismatches ) + this._drawMismatches = function() {}; + + }, + + _defaultConfig: function() { + return this._mergeConfigs( + dojo.clone( this.inherited(arguments) ), + { + //maxFeatureScreenDensity: 400 + style: { + color: function( feature, path, glyph, track ) { + var strand = feature.get('strand'); + if(Math.abs(strand) != 1 && strand != '+' && strand != '-') + return track.colorForBase('reference'); + else if(track.config.useXS) { + var xs = feature._get('xs') + var strand={'-':'color_rev_strand','+':'color_fwd_strand'}[xs]; + if(!strand) strand='color_nostrand'; + return glyph.getStyle( feature, strand ); + } + else if(feature.get('multi_segment_template')) { + var revflag=feature.get('multi_segment_first'); + if(feature.get('multi_segment_all_correctly_aligned')) { + if(revflag||!track.config.useReverseTemplate){ + return strand == 1 || strand == '+' + ? glyph.getStyle( feature, 'color_fwd_strand' ) + : glyph.getStyle( feature, 'color_rev_strand' ); + }else { + return strand == 1 || strand == '+' + ? glyph.getStyle( feature, 'color_rev_strand' ) + : glyph.getStyle( feature, 'color_fwd_strand' ); + } + } + if(feature.get('multi_segment_next_segment_unmapped')) { + if(revflag||!track.config.useReverseTemplate){ + return strand == 1 || strand == '+' + ? glyph.getStyle( feature, 'color_fwd_missing_mate' ) + : glyph.getStyle( feature, 'color_rev_missing_mate' ); + }else{ + return strand == 1 || strand == '+' + ? glyph.getStyle( feature, 'color_rev_missing_mate' ) + : glyph.getStyle( feature, 'color_fwd_missing_mate' ); + } + } + if(feature.get('seq_id') == feature.get('next_seq_id')) { + if(revflag||!track.config.useReverseTemplate){ + return strand == 1 || strand == '+' + ? glyph.getStyle( feature, 'color_fwd_strand_not_proper' ) + : glyph.getStyle( feature, 'color_rev_strand_not_proper' ); + }else{ + return strand == 1 || strand == '+' + ? glyph.getStyle( feature, 'color_rev_strand_not_proper' ) + : glyph.getStyle( feature, 'color_fwd_strand_not_proper' ); + } + } + // should only leave aberrant chr + return strand == 1 || strand == '+' + ? glyph.getStyle( feature, 'color_fwd_diff_chr' ) + : glyph.getStyle( feature, 'color_rev_diff_chr' ); + } + return strand == 1 || strand == '+' + ? glyph.getStyle( feature, 'color_fwd_strand' ) + : glyph.getStyle( feature, 'color_rev_strand' ); + }, + color_fwd_strand_not_proper: '#ECC8C8', + color_rev_strand_not_proper: '#BEBED8', + color_fwd_strand: '#EC8B8B', + color_rev_strand: '#8F8FD8', + color_fwd_missing_mate: '#D11919', + color_rev_missing_mate: '#1919D1', + color_fwd_diff_chr: '#000000', + color_rev_diff_chr: '#969696', + color_nostrand: '#999999', + border_color: null, + + strandArrow: false, + + height: 7, + marginBottom: 1, + showMismatches: true, + mismatchFont: 'bold 10px Courier New,monospace' + } + } + ); + }, + + renderFeature: function( context, fRect ) { + + this.inherited( arguments ); + + // draw some mismatches if the feature is more than 3px wide: + // draw everything if zoomed in past 0.2 px/bp, otherwise + // draw only skips and deletions (the mismatches that + // might be large enough to see) + if( fRect.w > 2 ) { + if( fRect.viewInfo.scale > 0.2 ) + this._drawMismatches( context, fRect, this._getMismatches( fRect.f ) ); + else + this._drawMismatches( context, fRect, this._getSkipsAndDeletions( fRect.f )); + } + }, + + // draw both gaps and mismatches + _drawMismatches: function( context, fRect, mismatches ) { + var feature = fRect.f; + var block = fRect.viewInfo.block; + var scale = block.scale; + + var charSize = this.getCharacterMeasurements( context ); + context.textBaseline = 'middle'; // reset to alphabetic (the default) after loop + + array.forEach( mismatches, function( mismatch ) { + var start = feature.get('start') + mismatch.start; + var end = start + mismatch.length; + + var mRect = { + h: (fRect.rect||{}).h || fRect.h, + l: block.bpToX( start ), + t: fRect.rect.t + }; + mRect.w = Math.max( block.bpToX( end ) - mRect.l, 1 ); + + if( mismatch.type == 'mismatch' || mismatch.type == 'deletion' ) { + context.fillStyle = this.track.colorForBase( mismatch.type == 'deletion' ? 'deletion' : mismatch.base ); + context.fillRect( mRect.l, mRect.t, mRect.w, mRect.h ); + + if( mRect.w >= charSize.w && mRect.h >= charSize.h-3 ) { + context.font = this.config.style.mismatchFont; + context.fillStyle = mismatch.type == 'deletion' ? 'white' : 'black'; + context.fillText( mismatch.base, mRect.l+(mRect.w-charSize.w)/2+1, mRect.t+mRect.h/2 ); + } + } + else if( mismatch.type == 'insertion' ) { + context.fillStyle = 'purple'; + context.fillRect( mRect.l-1, mRect.t+1, 2, mRect.h-2 ); + context.fillRect( mRect.l-2, mRect.t, 4, 1 ); + context.fillRect( mRect.l-2, mRect.t+mRect.h-1, 4, 1 ); + if( mRect.w >= charSize.w && mRect.h >= charSize.h-3 ) { + context.font = this.config.style.mismatchFont; + context.fillText( '('+mismatch.base+')', mRect.l+2, mRect.t+mRect.h/2 ); + } + } + else if( mismatch.type == 'hardclip' || mismatch.type == 'softclip' ) { + context.fillStyle = mismatch.type == 'hardclip' ? 'red' : 'blue'; + context.fillRect( mRect.l-1, mRect.t+1, 2, mRect.h-2 ); + context.fillRect( mRect.l-2, mRect.t, 4, 1 ); + context.fillRect( mRect.l-2, mRect.t+mRect.h-1, 4, 1 ); + if( mRect.w >= charSize.w && mRect.h >= charSize.h-3 ) { + context.font = this.config.style.mismatchFont; + context.fillText( '('+mismatch.base+')', mRect.l+2, mRect.t+mRect.h/2 ); + } + } + else if( mismatch.type == 'skip' ) { + context.clearRect( mRect.l, mRect.t, mRect.w, mRect.h ); + context.fillStyle = '#333'; + context.fillRect( mRect.l, mRect.t+(mRect.h-2)/2, mRect.w, 2 ); + } + },this); + + context.textBaseline = 'alphabetic'; + }, + + getCharacterMeasurements: function( context ) { + return this.charSize = this.charSize || function() { + var fpx; + + try { + fpx = (this.config.style.mismatchFont.match(/(\d+)px/i)||[])[1]; + } catch(e) {} + + fpx = fpx || Infinity; + return { w: fpx, h: fpx }; + }.call(this); + } + +}); +}); diff --git a/client/apollo/js/View/FeatureGlyph/SVG/Box.js b/client/apollo/js/View/FeatureGlyph/SVG/Box.js new file mode 100644 index 0000000000..140170931f --- /dev/null +++ b/client/apollo/js/View/FeatureGlyph/SVG/Box.js @@ -0,0 +1,166 @@ +define([ + 'dojo/_base/declare', + 'dojo/_base/array', + 'dojo/_base/lang', + 'JBrowse/Util/FastPromise', + 'WebApollo/View/FeatureGlyph', + './_FeatureLabelMixin' + ], + function( + declare, + array, + lang, + FastPromise, + FeatureGlyph, + FeatureLabelMixin + ) { + + +return declare([ FeatureGlyph, FeatureLabelMixin], { + + renderFeature: function( context, fRect ) { + if( this.track.displayMode != 'collapsed' ) + context.clearRect( Math.floor(fRect.l), fRect.t, Math.ceil(fRect.w-Math.floor(fRect.l)+fRect.l), fRect.h ); + + this.renderBox( context, fRect.viewInfo, fRect.f, fRect.t, fRect.rect.h, fRect.f ); + this.renderLabel( context, fRect ); + this.renderDescription( context, fRect ); + this.renderArrowhead( context, fRect ); + }, + + // top and height are in px + renderBox: function( context, viewInfo, feature, top, overallHeight, parentFeature, style ) { + var left = viewInfo.block.bpToX( feature.get('start') ); + var width = viewInfo.block.bpToX( feature.get('end') ) - left; + //left = Math.round( left ); + //width = Math.round( width ); + + style = style || lang.hitch( this, 'getStyle' ); + + var height = this._getFeatureHeight( viewInfo, feature ); + if( ! height ) + return; + if( height != overallHeight ) + top += Math.round( (overallHeight - height)/2 ); + + // background + var bgcolor = style( feature, 'color' ); + if( bgcolor ) { + context.fillStyle = bgcolor; + context.fillRect( left, top, Math.max(1,width), height ); + } + else { + context.clearRect( left, top, Math.max(1,width), height ); + } + + // foreground border + var borderColor, lineWidth; + if( (borderColor = style( feature, 'borderColor' )) && ( lineWidth = style( feature, 'borderWidth')) ) { + if( width > 3 ) { + context.lineWidth = lineWidth; + context.strokeStyle = borderColor; + + // need to stroke a smaller rectangle to remain within + // the bounds of the feature's overall height and + // width, because of the way stroking is done in + // canvas. thus the +0.5 and -1 business. + context.strokeRect( left+lineWidth/2, top+lineWidth/2, width-lineWidth, height-lineWidth ); + } + else { + context.globalAlpha = lineWidth*2/width; + context.fillStyle = borderColor; + context.fillRect( left, top, Math.max(1,width), height ); + context.globalAlpha = 1; + } + } + }, + + // feature label + renderLabel: function( context, fRect ) { + if( fRect.label ) { + context.font = fRect.label.font; + context.fillStyle = fRect.label.fill; + context.textBaseline = fRect.label.baseline; + context.fillText( fRect.label.text, + fRect.l+(fRect.label.xOffset||0), + fRect.t+(fRect.label.yOffset||0) + ); + } + }, + + // feature description + renderDescription: function( context, fRect ) { + if( fRect.description ) { + context.font = fRect.description.font; + context.fillStyle = fRect.description.fill; + context.textBaseline = fRect.description.baseline; + context.fillText( + fRect.description.text, + fRect.l+(fRect.description.xOffset||0), + fRect.t + (fRect.description.yOffset||0) + ); + } + }, + + // strand arrowhead + renderArrowhead: function( context, fRect ) { + if( fRect.strandArrow ) { + if( fRect.strandArrow == 1 && fRect.rect.l+fRect.rect.w <= context.canvas.width ) { + this.getEmbeddedImage( 'plusArrow' ) + .then( function( img ) { + context.imageSmoothingEnabled = false; + context.drawImage( img, fRect.rect.l + fRect.rect.w, fRect.t + (fRect.rect.h-img.height)/2 ); + }); + } + else if( fRect.strandArrow == -1 && fRect.rect.l >= 0 ) { + this.getEmbeddedImage( 'minusArrow' ) + .then( function( img ) { + context.imageSmoothingEnabled = false; + context.drawImage( img, fRect.rect.l-9, fRect.t + (fRect.rect.h-img.height)/2 ); + }); + } + } + }, + + updateStaticElements: function( context, fRect, viewArgs ) { + var vMin = viewArgs.minVisible; + var vMax = viewArgs.maxVisible; + var block = fRect.viewInfo.block; + + if( !( block.containsBp( vMin ) || block.containsBp( vMax ) ) ) + return; + + var scale = block.scale; + var bpToPx = viewArgs.bpToPx; + var lWidth = viewArgs.lWidth; + var labelBp = lWidth / scale; + var feature = fRect.f; + var fMin = feature.get('start'); + var fMax = feature.get('end'); + + if( fRect.strandArrow ) { + if( fRect.strandArrow == 1 && fMax >= vMax && fMin <= vMax ) { + this.getEmbeddedImage( 'plusArrow' ) + .then( function( img ) { + context.imageSmoothingEnabled = false; + context.drawImage( img, bpToPx(vMax) - bpToPx(vMin) - 9, fRect.t + (fRect.rect.h-img.height)/2 ); + }); + } + else if( fRect.strandArrow == -1 && fMin <= vMin && fMax >= vMin ) { + this.getEmbeddedImage( 'minusArrow' ) + .then( function( img ) { + context.imageSmoothingEnabled = false; + context.drawImage( img, 0, fRect.t + (fRect.rect.h-img.height)/2 ); + }); + } + } + + var fLabelWidth = fRect.label ? fRect.label.w : 0; + var fDescriptionWidth = fRect.description ? fRect.description.w : 0; + var maxLeft = bpToPx( fMax ) - Math.max(fLabelWidth, fDescriptionWidth) - bpToPx( vMin ); + var minLeft = bpToPx( fMin ) - bpToPx( vMin ); + + } + +}); +}); \ No newline at end of file diff --git a/client/apollo/js/View/FeatureGlyph/SVG/Diamond.js b/client/apollo/js/View/FeatureGlyph/SVG/Diamond.js new file mode 100644 index 0000000000..6e8785ac28 --- /dev/null +++ b/client/apollo/js/View/FeatureGlyph/SVG/Diamond.js @@ -0,0 +1,79 @@ +define(['dojo/_base/declare', + 'dojo/_base/lang', + './Box'], + function(declare, + lang, + Box) { + +return declare(Box, { + + renderBox: function( context, viewInfo, feature, top, overallHeight, parentFeature, style ) { + + var left = viewInfo.block.bpToX( feature.get('start') ); + var width = viewInfo.block.bpToX( feature.get('end') ) - left; + //left = Math.round( left ); + //width = Math.round( width ); + + style = style || lang.hitch( this, 'getStyle' ); + + var height = this._getFeatureHeight( viewInfo, feature ); + if( ! height ) + return; + if( height != overallHeight ) + top += Math.round( (overallHeight - height)/2 ); + + // background + var bgcolor = style( feature, 'color' ); + if( bgcolor ) { + context.fillStyle = bgcolor; + context.beginPath(); + context.moveTo(left,top+height/2); + context.lineTo(left + Math.max(1,width)/2,top); + context.lineTo(left+Math.max(1,width),top+height/2); + context.lineTo(left + Math.max(1,width)/2,top+height); + context.closePath(); + context.fill(); + } + else { + context.clearRect( left, top, Math.max(1,width), height ); + } + + // foreground border + var borderColor, lineWidth; + if( (borderColor = style( feature, 'borderColor' )) && ( lineWidth = style( feature, 'borderWidth')) ) { + if( width > 3 ) { + context.lineWidth = lineWidth; + context.strokeStyle = borderColor; + + // need to stroke a smaller rectangle to remain within + // the bounds of the feature's overall height and + // width, because of the way stroking is done in + // canvas. thus the +0.5 and -1 business. + //context.stroke(); + context.beginPath(); + context.moveTo(left,top+height/2); + context.lineTo(left + Math.max(1,width)/2,top); + context.lineTo(left+Math.max(1,width),top+height/2); + context.lineTo(left + Math.max(1,width)/2,top+height); + context.closePath(); + context.stroke(); + } + else { + context.globalAlpha = lineWidth*2/width; + context.fillStyle = borderColor; + context.beginPath(); + context.moveTo(left,top+height/2); + context.lineTo(left + Math.max(1,width)/2,top); + context.lineTo(left+Math.max(1,width),top+height/2); + context.lineTo(left + Math.max(1,width)/2,top+height); + context.closePath(); + context.fill(); + context.globalAlpha = 1; + + } + + } + } + +}); +}); diff --git a/client/apollo/js/View/FeatureGlyph/SVG/Gene.js b/client/apollo/js/View/FeatureGlyph/SVG/Gene.js new file mode 100644 index 0000000000..93d197711d --- /dev/null +++ b/client/apollo/js/View/FeatureGlyph/SVG/Gene.js @@ -0,0 +1,152 @@ +define([ + 'dojo/_base/declare', + 'dojo/_base/lang', + 'dojo/_base/array', + 'JBrowse/View/FeatureGlyph/Box', + 'JBrowse/View/FeatureGlyph/ProcessedTranscript' + ], + function( + declare, + lang, + array, + BoxGlyph, + ProcessedTranscriptGlyph + ) { + +return declare( BoxGlyph, { + +_defaultConfig: function() { + return this._mergeConfigs( + this.inherited(arguments), + { + transcriptType: 'mRNA', + style: { + transcriptLabelFont: 'normal 10px Univers,Helvetica,Arial,sans-serif', + transcriptLabelColor: 'black', + textFont: 'bold 12px Univers,Helvetica,Arial,sans-serif' + }, + labelTranscripts: true, + marginBottom: 0 + }); +}, + +_boxGlyph: function() { + return this.__boxGlyph || ( this.__boxGlyph = new BoxGlyph({ track: this.track, browser: this.browser, config: this.config }) ); +}, +_ptGlyph: function() { + return this.__ptGlyph || ( this.__ptGlyph = new ProcessedTranscriptGlyph({ track: this.track, browser: this.browser, config: this.config }) ); +}, + +_getFeatureRectangle: function( viewArgs, feature ) { + + // lay out rects for each of the subfeatures + var subArgs = lang.mixin( {}, viewArgs ); + subArgs.showDescriptions = subArgs.showLabels = false; + var subfeatures = feature.children(); + + // get the rects for the children + var padding = 1; + var fRect = { + l: 0, + h: 0, + r: 0, + w: 0, + subRects: [], + viewInfo: viewArgs, + f: feature, + glyph: this + }; + if( subfeatures && subfeatures.length ) { + // sort the children by name + subfeatures.sort( function( a, b ) { return (a.get('name') || a.get('id') || '').localeCompare( b.get('name') || b.get('id') || '' ); } ); + + fRect.l = Infinity; + fRect.r = -Infinity; + + var transcriptType = this.getConfForFeature( 'transcriptType', feature ); + for( var i = 0; i < subfeatures.length; i++ ) { + var subRect = ( subfeatures[i].get('type') == transcriptType + ? this._ptGlyph() + : this._boxGlyph() + )._getFeatureRectangle( subArgs, subfeatures[i] ); + + padding = i == subfeatures.length-1 ? 0 : 1; + subRect.t = subRect.rect.t = fRect.h && viewArgs.displayMode != 'collapsed' ? fRect.h+padding : 0; + + if( viewArgs.showLabels && this.getConfForFeature( 'labelTranscripts', subfeatures[i] ) ) { + var transcriptLabel = this.makeSideLabel( + this.getFeatureLabel(subfeatures[i]), + this.getStyle( subfeatures[i], 'transcriptLabelFont'), + subRect + ); + if( transcriptLabel ) { + transcriptLabel.fill = this.getStyle( subfeatures[i], 'transcriptLabelColor' ); + subRect.label = transcriptLabel; + subRect.l -= transcriptLabel.w; + subRect.w += transcriptLabel.w; + if( transcriptLabel.h > subRect.h ) + subRect.h = transcriptLabel.h; + transcriptLabel.yOffset = Math.floor(subRect.h/2); + transcriptLabel.xOffset = 0; + } + } + + fRect.subRects.push( subRect ); + fRect.r = Math.max( fRect.r, subRect.l+subRect.w-1 ); + fRect.l = Math.min( fRect.l, subRect.l ); + fRect.h = subRect.t+subRect.h+padding; + } + } + + // calculate the width + fRect.w = Math.max( fRect.r - fRect.l + 1, 2 ); + delete fRect.r; + fRect.rect = { l: fRect.l, h: fRect.h, w: fRect.w }; + if( viewArgs.displayMode != 'compact' ) + fRect.h += this.getStyle( feature, 'marginBottom' ) || 0; + + // no labels or descriptions if displayMode is collapsed, so stop here + if( viewArgs.displayMode == "collapsed") + return fRect; + + // expand the fRect to accommodate labels if necessary + this._expandRectangleWithLabels( viewArgs, feature, fRect ); + this._addMasksToRect( viewArgs, feature, fRect ); + + return fRect; +}, + +layoutFeature: function( viewInfo, layout, feature ) { + var fRect = this.inherited( arguments ); + if( fRect ) + array.forEach( fRect.subRects, function( subrect ) { + subrect.t += fRect.t; + subrect.rect.t += fRect.t; + }); + return fRect; +}, + +renderFeature: function( context, fRect ) { + if( fRect.viewInfo.displayMode != 'collapsed' ) + context.clearRect( Math.floor(fRect.l), fRect.t, Math.ceil(fRect.w-Math.floor(fRect.l)+fRect.l), fRect.h ); + + var subRects = fRect.subRects; + for( var i = 0; i < subRects.length; i++ ) { + subRects[i].glyph.renderFeature( context, subRects[i] ); + } + + this.renderLabel( context, fRect ); + this.renderDescription( context, fRect ); +}, + +updateStaticElements: function( context, fRect, viewArgs ) { + this.inherited( arguments ); + + var subRects = fRect.subRects; + for( var i = 0; i < subRects.length; i++ ) { + subRects[i].glyph.updateStaticElements( context, subRects[i], viewArgs ); + } +} + +}); +}); diff --git a/client/apollo/js/View/FeatureGlyph/SVG/ProcessedTranscript.js b/client/apollo/js/View/FeatureGlyph/SVG/ProcessedTranscript.js new file mode 100644 index 0000000000..5a962adaeb --- /dev/null +++ b/client/apollo/js/View/FeatureGlyph/SVG/ProcessedTranscript.js @@ -0,0 +1,252 @@ +define([ + 'dojo/_base/declare', + 'dojo/_base/array', + + 'dojox/color/Palette', + + 'JBrowse/Model/SimpleFeature', + 'JBrowse/View/FeatureGlyph/Segments' + ], + function( + declare, + array, + + Palette, + + SimpleFeature, + SegmentsGlyph + ) { + +return declare( SegmentsGlyph, { +_defaultConfig: function() { + return this._mergeConfigs( + this.inherited(arguments), + { + style: { + utrColor: function( feature, variable, glyph, track ) { + return glyph._utrColor( glyph.getStyle( feature.parent(), 'color' ) ).toString(); + } + }, + + subParts: 'CDS, UTR, five_prime_UTR, three_prime_UTR', + + impliedUTRs: false, + + inferCdsParts: false + }); +}, + +_getSubparts: function( f ) { + var c = f.children(); + if( ! c ) return []; + + if( c && this.config.inferCdsParts ) + c = this._makeCDSs( f, c ); + + if( c && this.config.impliedUTRs ) + c = this._makeUTRs( f, c ); + + var filtered = []; + for( var i = 0; i subparts[i].get('start') ) + codeStart = subparts[i].get('start'); + if( codeEnd < subparts[i].get('end') ) + codeEnd = subparts[i].get('end'); + } + else { + if( /exon/i.test( type ) ) { + exons.push( subparts[i] ); + } + } + } + + // splice out unspliced cds parts + codeIndices.sort( function(a,b) { return b - a; } ); + for ( i = codeIndices.length - 1; i >= 0; i-- ) + subparts.splice(codeIndices[i], 1); + + // bail if we don't have exons and cds + if( !( exons.length && codeStart < Infinity && codeEnd > -Infinity ) ) + return subparts; + + // make sure the exons are sorted by coord + exons.sort( function(a,b) { return a.get('start') - b.get('start'); } ); + + // iterate thru exons again, and calculate cds parts + var strand = parent.get('strand'); + var codePartStart = Infinity, + codePartEnd = -Infinity; + for ( i = 0; i < exons.length; i++ ) { + var start = exons[i].get('start'); + var end = exons[i].get('end'); + + // CDS containing exon + if( codeStart >= start && codeEnd <= end ) { + codePartStart = codeStart; + codePartEnd = codeEnd; + } + // 5' terminal CDS part + else if( codeStart >= start && codeStart < end ) { + codePartStart = codeStart; + codePartEnd = end; + } + // 3' terminal CDS part + else if( codeEnd > start && codeEnd <= end ) { + codePartStart = start; + codePartEnd = codeEnd; + } + // internal CDS part + else if( start < codeEnd && end > codeStart ) { + codePartStart = start; + codePartEnd = end; + } + + // "splice in" the calculated cds part into subparts + // at beginning of _makeCDSs() method, bail if cds subparts are encountered + subparts.splice(i, 0, ( new SimpleFeature( + { parent: parent, + data: { + start: codePartStart, + end: codePartEnd, + strand: strand, + type: 'CDS', + name: parent.get('uniqueID') + ":CDS:" + i + }}))); + } + + // make sure the subparts are sorted by coord + subparts.sort( function(a,b) { return a.get('start') - b.get('start'); } ); + + return subparts; +}, + +_makeUTRs: function( parent, subparts ) { + // based on Lincoln's UTR-making code in Bio::Graphics::Glyph::processed_transcript + + var codeStart = Infinity, + codeEnd = -Infinity; + + var i; + + var haveLeftUTR, haveRightUTR; + + // gather exons, find coding start and end, and look for UTRs + var type, exons = []; + for( i = 0; i subparts[i].get('start') ) + codeStart = subparts[i].get('start'); + if( codeEnd < subparts[i].get('end') ) + codeEnd = subparts[i].get('end'); + } + else if( /exon/i.test( type ) ) { + exons.push( subparts[i] ); + } + else if( this._isUTR( subparts[i] ) ) { + haveLeftUTR = subparts[i].get('start') == parent.get('start'); + haveRightUTR = subparts[i].get('end') == parent.get('end'); + } + } + + // bail if we don't have exons and CDS + if( !( exons.length && codeStart < Infinity && codeEnd > -Infinity ) ) + return subparts; + + // make sure the exons are sorted by coord + exons.sort( function(a,b) { return a.get('start') - b.get('start'); } ); + + var strand = parent.get('strand'); + + // make the left-hand UTRs + var start, end; + if( ! haveLeftUTR ) + for (i=0; i= codeStart ) break; + end = codeStart > exons[i].get('end') ? exons[i].get('end') : codeStart; + + subparts.unshift( new SimpleFeature( + { parent: parent, + data: { + start: start, + end: end, + strand: strand, + type: strand >= 0 ? 'five_prime_UTR' : 'three_prime_UTR' + }})); + } + + // make the right-hand UTRs + if( ! haveRightUTR ) + for (i=exons.length-1; i>=0; i--) { + end = exons[i].get('end'); + if( end <= codeEnd ) break; + + start = codeEnd < exons[i].get('start') ? exons[i].get('start') : codeEnd; + subparts.push( new SimpleFeature( + { parent: parent, + data: { + start: start, + end: end, + strand: strand, + type: strand >= 0 ? 'three_prime_UTR' : 'five_prime_UTR' + }})); + } + + return subparts; +}, + +_utrColor: function( baseColor ) { + return (this._palette || (this._palette = Palette.generate( baseColor, "splitComplementary"))).colors[1]; +}, + +_isUTR: function( feature ) { + return /(\bUTR|_UTR|untranslated[_\s]region)\b/.test( feature.get('type') || '' ); +}, + +getStyle: function( feature, name ) { + if( name == 'color' ) { + if( this._isUTR( feature ) ) { + return this.getStyle( feature, 'utrColor' ); + } + } + + return this.inherited(arguments); +}, + +_getFeatureHeight: function( viewInfo, feature ) { + var height = this.inherited( arguments ); + + if( this._isUTR( feature ) ) + return height*0.65; + + return height; +} + +}); +}); diff --git a/client/apollo/js/View/FeatureGlyph/SVG/Segments.js b/client/apollo/js/View/FeatureGlyph/SVG/Segments.js new file mode 100644 index 0000000000..6bedaa0640 --- /dev/null +++ b/client/apollo/js/View/FeatureGlyph/SVG/Segments.js @@ -0,0 +1,124 @@ +define([ + 'dojo/_base/declare', + 'dojo/_base/lang', + 'dojo/_base/array', + 'JBrowse/View/FeatureGlyph/Box' + ], + function( + declare, + lang, + array, + BoxGlyph + ) { + +return declare( BoxGlyph, { +_defaultConfig: function() { + return this._mergeConfigs( + this.inherited(arguments), + { + style: { + connectorColor: '#333', + connectorThickness: 1, + borderColor: 'rgba( 0, 0, 0, 0.3 )' + }, + subParts: function() { return true; } // accept all subparts by default + }); +}, + +renderFeature: function( context, fRect ) { + if( this.track.displayMode != 'collapsed' ) + context.clearRect( Math.floor(fRect.l), fRect.t, Math.ceil(fRect.w), fRect.h ); + + this.renderConnector( context, fRect ); + this.renderSegments( context, fRect ); + this.renderLabel( context, fRect ); + this.renderDescription( context, fRect ); + this.renderArrowhead( context, fRect ); +}, + +renderConnector: function( context, fRect ) { + // connector + var connectorColor = this.getStyle( fRect.f, 'connectorColor' ); + if( connectorColor ) { + context.fillStyle = connectorColor; + var connectorThickness = this.getStyle( fRect.f, 'connectorThickness' ); + context.fillRect( + fRect.rect.l, // left + Math.round(fRect.rect.t+(fRect.rect.h-connectorThickness)/2), // top + fRect.rect.w, // width + connectorThickness + ); + } +}, + +renderSegments: function( context, fRect ) { + var subparts = this._getSubparts( fRect.f ); + if( ! subparts.length ) return; + + var thisB = this; + var parentFeature = fRect.f; + function style( feature, stylename ) { + if( stylename == 'height' ) + return thisB._getFeatureHeight( fRect.viewInfo, feature ); + + return thisB.getStyle( feature, stylename ) || thisB.getStyle( parentFeature, stylename ); + } + + for( var i = 0; i < subparts.length; ++i ) { + this.renderBox( context, fRect.viewInfo, subparts[i], fRect.t, fRect.rect.h, fRect.f, style ); + } +}, + +_getSubparts: function( f ) { + var c = f.children(); + if( ! c ) return []; + + var filtered = []; + for( var i = 0; i 0 ) + text = text.slice( 0, text.length - excessCharacters - 1 ) + '…'; + + return { + text: text, + font: font, + baseline: 'middle', + w: dims.w * text.length, + h: dims.h + }; + }, + + /** + * Makes a label that lays across the bottom edge of a feature, + * respecting maxFeatureGlyphExpansion. + */ + makeBottomOrTopLabel: function( text, font, fRect ) { + if( ! text ) return null; + + var dims = this.measureFont( font ); + var excessCharacters = Math.round(( text.length * dims.w - fRect.w - this.track.getConf('maxFeatureGlyphExpansion') ) / dims.w ); + if( excessCharacters > 0 ) + text = text.slice( 0, text.length - excessCharacters - 1 ) + '…'; + + return { + text: text, + font: font, + baseline: 'bottom', + w: dims.w * text.length, + h: dims.h + }; + }, + + /** + * Makes a label that can be put in a popup or tooltip, + * not respecting maxFeatureGlyphExpansion or the width of the fRect. + */ + makePopupLabel: function( text, font ) { + if( ! text ) return null; + var dims = this.measureFont( font ); + return { + text: text, + font: font, + w: dims.w * text.length, + h: dims.h + } + }, + + /** + * Return an object with average `h` and `w` of characters in the + * font described by the given string. + */ + measureFont: function( font ) { + return fontMeasurementsCache[ font ] + || ( fontMeasurementsCache[font] = function() { + var ctx = document.createElement('canvas').getContext('2d'); + ctx.font = font; + var testString = "MMMMMMMMMMMMXXXXXXXXXX1234567890-.CGCC12345"; + var m = ctx.measureText( testString ); + return { + h: m.height || parseInt( font.match(/(\d+)px/)[1] ), + w: m.width / testString.length + }; + }.call( this )); + } +}); +}); diff --git a/client/apollo/js/View/SVGFeatureGlyph.js b/client/apollo/js/View/SVGFeatureGlyph.js new file mode 100644 index 0000000000..929d2057ea --- /dev/null +++ b/client/apollo/js/View/SVGFeatureGlyph.js @@ -0,0 +1,59 @@ +define([ + 'dojo/_base/declare', + 'dojo/_base/array', + 'JBrowse/View/FeatureGlyph' + ], + function( + declare, + array, + FeatureGlyph + ) { + +return declare( [FeatureGlyph], { + //stub + // renderFeature: function( context, fRect ) { + // }, + + /* If it's a boolean track, mask accordingly */ + maskBySpans: function( context, fRect ) { + var canvasHeight = context.canvas.height; + + var thisB = this; + + // make a temporary canvas to store image data + var tempCan = dojo.create( 'canvas', {height: canvasHeight, width: context.canvas.width} ); + var ctx2 = tempCan.getContext('2d'); + var l = Math.floor(fRect.l); + var w = Math.ceil(fRect.w + fRect.l) - l; + + /* note on the above: the rightmost pixel is determined + by l+w. If either of these is a float, then canvas + methods will not behave as desired (i.e. clear and + draw will not treat borders in the same way).*/ + array.forEach( fRect.m, function(m) { try { + if ( m.l < l ) { + m.w += m.l-l; + m.l = l; + } + if ( m.w > w ) + m.w = w; + if ( m.l < 0 ) { + m.w += m.l; + m.l = 0; + } + if ( m.l + m.w > l + w ) + m.w = w + l - m.l; + if ( m.l + m.w > context.canvas.width ) + m.w = context.canvas.width-m.l; + ctx2.drawImage(context.canvas, m.l, fRect.t, m.w, fRect.h, m.l, fRect.t, m.w, fRect.h); + context.globalAlpha = thisB.booleanAlpha; + // clear masked region and redraw at lower opacity. + context.clearRect(m.l, fRect.t, m.w, fRect.h); + context.drawImage(tempCan, m.l, fRect.t, m.w, fRect.h, m.l, fRect.t, m.w, fRect.h); + context.globalAlpha = 1; + } catch(e) {}; + }); + }, + +}); +}); From 125923d173f19cd1c5caa1bf030b41e86ca9aea1 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Mon, 18 Dec 2017 17:11:35 -0800 Subject: [PATCH 14/21] added some debugging and cleaned up code --- .../apollo/js/Store/SeqFeature/GFF3/Parser.js | 3 + .../apollo/js/Store/SeqFeature/GFF3Tabix.js | 62 +++++-------------- 2 files changed, 18 insertions(+), 47 deletions(-) diff --git a/client/apollo/js/Store/SeqFeature/GFF3/Parser.js b/client/apollo/js/Store/SeqFeature/GFF3/Parser.js index b8061d8399..18813c086e 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3/Parser.js +++ b/client/apollo/js/Store/SeqFeature/GFF3/Parser.js @@ -92,6 +92,8 @@ return declare( [Parser], { }, _return_item: function(i) { + console.log('returning: '); + console.log(i); if( i[0] ) this.featureCallback( i ); else if( i.directive ) @@ -101,6 +103,7 @@ return declare( [Parser], { }, finish: function() { + console.log('finishing'); this._return_all_under_construction_features(); this.endCallback(); }, diff --git a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js index 56bb7a3a49..359ff4f67d 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js +++ b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js @@ -82,6 +82,9 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], featureCallback: function(fs) { array.forEach( fs, function( feature ) { var feat = thisB._formatFeature(feature); + console.log('doing feature callback'); + console.log(feature); + console.log(feat); f(feat); }); }, @@ -101,8 +104,10 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], chrName = sequenceListObject[0].name ; reverse = sequenceListObject[0].reverse; var unprojectedArray = ProjectionUtils.unProjectCoordinates(refSeqName,query.start,query.end); + console.log('input query: '+ refSeqName + ' ' + query.start + ' ' + query.end); min = unprojectedArray[0]; max = unprojectedArray[1]; + console.log('projected: '+ chrName+ ' ' + min + ' ' + max); } else{ min = query.start ; @@ -112,66 +117,29 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], // console.log(min + ' ' + max); + var minLine = -1 ; + var maxLine = -1 ; + thisB.indexedData.getLines( chrName, min, max, function( line ) { + minLine = minLine === -1 || line.start < minLine ? line.start : minLine ; + maxLine = maxLine === -1 || line.end > maxLine ? line.end : maxLine ; parser._buffer_feature( thisB.lineToFeature(line)); }, function() { + // console.log('wrapup') + // console.log(min + ' A->'+ minLine); + // console.log(max + ' B->'+ maxLine); + // if(minLine >= min && maxLine <= max){ parser.finish(); + // } }, errorCallback ); }, errorCallback ); - }, - - lineToFeature: function( line ) { - var attributes = GFF3.parse_attributes( line.fields[8] ); - var ref = line.fields[0]; - var source = line.fields[1]; - var type = line.fields[2]; - var strand = {'-':-1,'.':0,'+':1}[line.fields[6]]; - var remove_id; - if( !attributes.ID ) { - attributes.ID = [line.fields.join('/')]; - remove_id = true; - } - - var refSeqName = this.refSeq.name ; - var min, max ; - var chrName; - if(ProjectionUtils.isSequenceList(refSeqName)){ - var sequenceListObject = ProjectionUtils.parseSequenceList(refSeqName); - chrName = sequenceListObject[0].name ; - if(sequenceListObject[0].reverse){ - strand = ProjectionUtils.flipStrand(strand); - // strand = -1 * strand ; - } - // var unprojectedArray = ProjectionUtils.unProjectCoordinates(refSeqName,line.start,line.end); - // min = unprojectedArray[0]; - // max = unprojectedArray[1]; - } - else{ - chrName = query.ref ; - } - min = line.start; - max = line.end ; - - var featureData = { - start: min, - end: max, - strand: strand, - child_features: [], - seq_id: chrName, - attributes: attributes, - type: type, - source: source, - remove_id: remove_id - }; - - return featureData; } From 14675ef75ae0ad8c42103c56e8db0a22aa45f69d Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Mon, 18 Dec 2017 17:30:20 -0800 Subject: [PATCH 15/21] resolving feature --- client/apollo/js/View/FeatureGlyph/SVG/Box.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/apollo/js/View/FeatureGlyph/SVG/Box.js b/client/apollo/js/View/FeatureGlyph/SVG/Box.js index 140170931f..fb67ea36da 100644 --- a/client/apollo/js/View/FeatureGlyph/SVG/Box.js +++ b/client/apollo/js/View/FeatureGlyph/SVG/Box.js @@ -3,7 +3,7 @@ define([ 'dojo/_base/array', 'dojo/_base/lang', 'JBrowse/Util/FastPromise', - 'WebApollo/View/FeatureGlyph', + 'WebApollo/View/SVGFeatureGlyph', './_FeatureLabelMixin' ], function( From bd3d7846cba7cce27cdf41d336f98f70b1b8310f Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Thu, 21 Dec 2017 11:36:19 -0800 Subject: [PATCH 16/21] properly stitching range using HTMLFeatures --- .../apollo/js/Store/SeqFeature/GFF3/Parser.js | 3 -- .../apollo/js/Store/SeqFeature/GFF3Tabix.js | 45 ++++++++++++------- .../js/View/Track/WebApolloCanvasFeatures.js | 6 +++ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/client/apollo/js/Store/SeqFeature/GFF3/Parser.js b/client/apollo/js/Store/SeqFeature/GFF3/Parser.js index 18813c086e..b8061d8399 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3/Parser.js +++ b/client/apollo/js/Store/SeqFeature/GFF3/Parser.js @@ -92,8 +92,6 @@ return declare( [Parser], { }, _return_item: function(i) { - console.log('returning: '); - console.log(i); if( i[0] ) this.featureCallback( i ); else if( i.directive ) @@ -103,7 +101,6 @@ return declare( [Parser], { }, finish: function() { - console.log('finishing'); this._return_all_under_construction_features(); this.endCallback(); }, diff --git a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js index 359ff4f67d..3cd011bef5 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js +++ b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js @@ -74,6 +74,7 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], // }, + _getFeatures: function( query, featureCallback, finishedCallback, errorCallback ) { var thisB = this; var f=featureCallback; @@ -82,9 +83,9 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], featureCallback: function(fs) { array.forEach( fs, function( feature ) { var feat = thisB._formatFeature(feature); - console.log('doing feature callback'); - console.log(feature); - console.log(feat); + // console.log('doing feature callback'); + // console.log(feature); + // console.log(feat); f(feat); }); }, @@ -104,10 +105,10 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], chrName = sequenceListObject[0].name ; reverse = sequenceListObject[0].reverse; var unprojectedArray = ProjectionUtils.unProjectCoordinates(refSeqName,query.start,query.end); - console.log('input query: '+ refSeqName + ' ' + query.start + ' ' + query.end); + // console.log('input query: '+ refSeqName + ' ' + query.start + ' ' + query.end); min = unprojectedArray[0]; max = unprojectedArray[1]; - console.log('projected: '+ chrName+ ' ' + min + ' ' + max); + // console.log('projected: '+ chrName+ ' ' + min + ' ' + max); } else{ min = query.start ; @@ -120,25 +121,37 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], var minLine = -1 ; var maxLine = -1 ; + var estimabeBlocks = function(line){ + minLine = minLine === -1 || line.start < minLine ? line.start : minLine ; + maxLine = maxLine === -1 || line.end > maxLine ? line.end : maxLine ; + }; + + var handleLines = function(line){ + parser._buffer_feature( thisB.lineToFeature(line)); + }; + thisB.indexedData.getLines( chrName, min, max, - function( line ) { - minLine = minLine === -1 || line.start < minLine ? line.start : minLine ; - maxLine = maxLine === -1 || line.end > maxLine ? line.end : maxLine ; - parser._buffer_feature( thisB.lineToFeature(line)); - }, + estimabeBlocks, function() { - // console.log('wrapup') - // console.log(min + ' A->'+ minLine); - // console.log(max + ' B->'+ maxLine); - // if(minLine >= min && maxLine <= max){ - parser.finish(); - // } + // parser.finish(); + thisB.indexedData.getLines( + chrName, + minLine, + maxLine, + handleLines, + function() { + parser.finish(); + }, + errorCallback + ); }, errorCallback ); + + }, errorCallback ); } diff --git a/client/apollo/js/View/Track/WebApolloCanvasFeatures.js b/client/apollo/js/View/Track/WebApolloCanvasFeatures.js index ece4b24c17..68d8e55d76 100644 --- a/client/apollo/js/View/Track/WebApolloCanvasFeatures.js +++ b/client/apollo/js/View/Track/WebApolloCanvasFeatures.js @@ -173,10 +173,14 @@ return declare( CanvasFeaturesTrack, start: Math.max( 0, leftBase - bpExpansion ), end: rightBase + bpExpansion }; + console.log('getting feature for region '); + console.log(region); this.store.getFeatures( region, function( feature ) { + console.log('returning feature: '); + console.log(feature ); if( thisB.destroyed || ! thisB.filterFeature( feature ) ) return; @@ -185,6 +189,8 @@ return declare( CanvasFeaturesTrack, var rectNumber = fRects.length-1; feature = ProjectionUtils.projectJSONFeature(feature,refSeqName); + console.log('projected feature: '); + console.log(feature ); // get the appropriate glyph object to render this feature thisB.getGlyph( From d1760cd57d21048cfd80049afac886e0b359046a Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Fri, 22 Dec 2017 15:06:55 -0800 Subject: [PATCH 17/21] fixed subfeature depths --- .../js/View/Track/DraggableProjectedHTMLFeatures.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js b/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js index e7a90ee350..dc297a2f38 100644 --- a/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js +++ b/client/apollo/js/View/Track/DraggableProjectedHTMLFeatures.js @@ -1026,15 +1026,16 @@ var draggableTrack = declare( HTMLFeatureTrack, // if (subDiv && wholeCDS && (subtype === "exon")) { // if (wholeCDS && (subtype === "exon")) { // pass even if subDiv is null (not drawn), in order to correctly calc downstream CDS frame + if(subtype === 'mRNA'){ + console.log('handling the subfeature from '+feature._uniqueID + ' into '+subfeat._uniqueID); + this.handleSubFeatures(subfeat,featDiv,displayStart,displayEnd,block); + } + else // CHANGED to call renderExonSegments even if no wholeCDS -- // non wholeCDS means undefined cdsMin, which will trigger creation of UTR div for entire exon if (subtype === "exon") { // pass even if subDiv is null (not drawn), in order to correctly calc downstream CDS frame priorCdsLength = this.renderExonSegments(subfeat, subDiv, cdsMin, cdsMax, displayStart, displayEnd, priorCdsLength, reverse); } - if(subtype === 'mRNA'){ - console.log('handling the subfeature from '+feature._uniqueID + ' into '+subfeat._uniqueID); - this.handleSubFeatures(subfeat,subDiv,displayStart,displayEnd,block); - } if (this.verbose_render) { console.log("in DraggableFeatureTrack.handleSubFeatures, subDiv: "); console.log(subDiv); From a8ffda62074fbc960299de3f9eeee716709637a6 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Sat, 23 Dec 2017 06:35:41 -0800 Subject: [PATCH 18/21] a very simple stub of a cache --- .../apollo/js/Store/SeqFeature/GFF3Tabix.js | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js index 3cd011bef5..b2227ff048 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js +++ b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js @@ -130,26 +130,47 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], parser._buffer_feature( thisB.lineToFeature(line)); }; - thisB.indexedData.getLines( - chrName, - min, - max, - estimabeBlocks, - function() { - // parser.finish(); - thisB.indexedData.getLines( - chrName, - minLine, - maxLine, - handleLines, - function() { - parser.finish(); - }, - errorCallback - ); - }, - errorCallback - ); + var lineCache = this.lineCache = this.lineCache || {}; + var lineKey = chrName +':' + min + '..' + max ; + var line = lineCache[lineKey]; + if(line){ + console.log('USING CACHE: '+lineKey); + thisB.indexedData.getLines( + chrName, + line.min, + line.max, + handleLines, + function() { + parser.finish(); + }, + errorCallback + ); + } + else{ + console.log('not USING CACHE: '+lineKey); + thisB.indexedData.getLines( + chrName, + min, + max, + estimabeBlocks, + function() { + lineCache[lineKey] = {min:min,max:max,chrName:chrName}; + thisB.indexedData.getLines( + chrName, + minLine, + maxLine, + handleLines, + function() { + parser.finish(); + }, + errorCallback + ); + }, + errorCallback + ); + } + + }, errorCallback ); From 42d3fd06bf45e72558319a152f12fc1b31a7cd7b Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Mon, 8 Jan 2018 10:42:16 -0800 Subject: [PATCH 19/21] fixed typo --- client/apollo/js/Store/SeqFeature/GFF3Tabix.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js index b2227ff048..c9c89f4358 100644 --- a/client/apollo/js/Store/SeqFeature/GFF3Tabix.js +++ b/client/apollo/js/Store/SeqFeature/GFF3Tabix.js @@ -121,7 +121,7 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], var minLine = -1 ; var maxLine = -1 ; - var estimabeBlocks = function(line){ + var estimableBlocks = function(line){ minLine = minLine === -1 || line.start < minLine ? line.start : minLine ; maxLine = maxLine === -1 || line.end > maxLine ? line.end : maxLine ; }; @@ -152,7 +152,7 @@ return declare( [ GFF3Tabix , GlobalStatsEstimationMixin ], chrName, min, max, - estimabeBlocks, + estimableBlocks, function() { lineCache[lineKey] = {min:min,max:max,chrName:chrName}; thisB.indexedData.getLines( From e0c73792c5c525b7bb8c01cfe4f4c78df7f85c66 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Mon, 8 Jan 2018 15:36:00 -0800 Subject: [PATCH 20/21] able to handle gene root types properly when annotating --- client/apollo/js/JSONUtils.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/client/apollo/js/JSONUtils.js b/client/apollo/js/JSONUtils.js index d6ad6224bc..a4f7a6515a 100644 --- a/client/apollo/js/JSONUtils.js +++ b/client/apollo/js/JSONUtils.js @@ -245,6 +245,18 @@ JSONUtils.createApolloFeature = function( jfeature, specified_type, useName, spe console.log(jfeature); } + // if specified_type is not 'gene' or 'pseudo-gene' and we find 'gene' or pseudogene, then we need to replace jfeature with the sub-type + if(jfeature.get('type')==='gene' || jfeature.get('type')==='pseudogene'){ + var root_sub_features = jfeature.get('subfeatures'); + if(root_sub_features.length===1){ + jfeature = root_sub_features[0]; + } + else{ + console.log('Problem creating Apollo, wrong number of subfeatures found'); + console.log(root_sub_features); + } + } + var afeature = new Object(); var astrand; // Apollo feature strand must be an integer @@ -312,7 +324,7 @@ JSONUtils.createApolloFeature = function( jfeature, specified_type, useName, spe // use filteredsubs if present instead of subfeats? // if (jfeature.filteredsubs) { subfeats = jfeature.filteredsubs; } // else { subfeats = jfeature.get('subfeatures'); } - subfeats = jfeature.get('subfeatures'); + subfeats = jfeature.get('subfeatures'); if( subfeats && subfeats.length ) { afeature.children = []; var slength = subfeats.length; @@ -387,7 +399,7 @@ JSONUtils.createApolloFeature = function( jfeature, specified_type, useName, spe foundExons = true; } if (converted_subtype) { - afeature.children.push( JSONUtils.createApolloFeature( subfeat, converted_subtype ) ); + afeature.children.push( JSONUtils.createApolloFeature( subfeat, converted_subtype ) ); if (diagnose) { console.log(" subfeat original type: " + subtype + ", converted type: " + converted_subtype); } } else { From 4413e205267ace475973ddb4e815cca31a8d6d33 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Tue, 9 Jan 2018 18:02:16 -0800 Subject: [PATCH 21/21] updated track styles to not be so ugly if rendering the gene first --- client/apollo/css/webapollo_track_styles.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/apollo/css/webapollo_track_styles.css b/client/apollo/css/webapollo_track_styles.css index c4be8e0bd0..492498e76a 100644 --- a/client/apollo/css/webapollo_track_styles.css +++ b/client/apollo/css/webapollo_track_styles.css @@ -49,7 +49,7 @@ div.track_localannot { background-color: #DDDDCC; } -/* +/* To support WebApollo with sequence alteration features shown on SequenceTrack, sequence style MUST NOT have a z-index specified */ @@ -189,12 +189,15 @@ div.annot-sequence { * (currently only subfeatures like this are exons, which have CDS and UTR child divs) */ .container-100pct, -.plus-container-100pct, +.plus-container-100pct, +.plus-mRNA, +.minus-mRNA, .minus-container-100pct { height: 100%; background-color: transparent; } + .feature-render { position: absolute; min-width: 1px;