UNPKG

@gmod/jbrowse

Version:

JBrowse - client-side genome browser

371 lines (329 loc) 13.6 kB
define( [ 'dojo/_base/declare', 'dojo/_base/array', 'dojo/_base/lang', 'dojo/dom-construct', 'dojo/dom-class', 'dojo/query', 'JBrowse/View/Track/BlockBased', 'JBrowse/View/Track/_ExportMixin', 'JBrowse/CodonTable', 'JBrowse/Util' ], function( declare, array, lang, dom, domClass, query, BlockBased, ExportMixin, CodonTable, Util ) { return declare( [BlockBased, ExportMixin, CodonTable], /** * @lends JBrowse.View.Track.Sequence.prototype */ { /** * Track to display the underlying reference sequence, when zoomed in * far enough. * * @constructs * @extends JBrowse.View.Track.BlockBased */ constructor: function( args ) { this._charMeasurements = {}; this._codonTable = this.generateCodonTable(lang.mixin(this.defaultCodonTable,this.config.codonTable)); this._codonStarts = this.config.codonStarts || this.defaultStarts this._codonStops = this.config.codonStops || this.defaultStops }, _defaultConfig: function() { return { maxExportSpan: 500000, showForwardStrand: true, showReverseStrand: true, showTranslation: true, showColor: true, seqType: 'dna', proteinColorScheme: 'taylor' }; }, _exportFormats: function() { return [{name: 'FASTA', label: 'FASTA', fileExt: 'fasta'}]; }, endZoom: function(destScale, destBlockBases) { this.clear(); }, setViewInfo:function(genomeView, heightUpdate, numBlocks, trackDiv, widthPct, widthPx, scale) { this.inherited( arguments ); this.show(); }, nbsp: String.fromCharCode(160), fillBlock:function( args ) { var blockIndex = args.blockIndex; var block = args.block; var leftBase = args.leftBase; var rightBase = args.rightBase; var scale = args.scale; var leftExtended = leftBase - 2; var rightExtended = rightBase + 2; var thisB = this; var blur = dojo.create( 'div', { className: 'sequence_blur', innerHTML: '<span class="loading">Loading</span>' }, block.domNode ); this.heightUpdate( blur.offsetHeight+2*blur.offsetTop, blockIndex ); // if we are zoomed in far enough to draw bases, then draw them if ( scale >= 1.3 ) { this.store.getReferenceSequence( { ref: this.refSeq.name, start: leftExtended, end: rightExtended }, function( seq ) { if(seq.trim() == ""){ blur.innerHTML = '<span class="zoom">No sequence available</span>';; } else { dom.empty( block.domNode ); thisB._fillSequenceBlock( block, blockIndex, scale, seq ); } args.finishCallback(); }, function(error) { if (args.errorCallback) args.errorCallback(error) else { console.error(error) args.finishCallback() } } ); } // otherwise, just draw a sort of line (possibly dotted) that // suggests there are bases there if you zoom in far enough else { blur.innerHTML = '<span class="zoom">Zoom in to see sequence</span>'; args.finishCallback(); } }, _fillSequenceBlock: function( block, blockIndex, scale, seq ) { seq = seq.replace(/\s/g,this.nbsp); var blockStart = block.startBase; var blockEnd = block.endBase; var blockSeq = seq.substring( 2, seq.length - 2 ); var blockLength = blockSeq.length; var extStart = blockStart-2; var extEnd = blockStart+2; var leftover = (seq.length - 2) % 3; var extStartSeq = seq.substring( 0, seq.length - 2 ); var extEndSeq = seq.substring( 2 ); if( this.config.showForwardStrand && this.config.showTranslation ) { var frameDiv = []; for( var i = 0; i < 3; i++ ) { var transStart = blockStart + i; var frame = (transStart % 3 + 3) % 3; var translatedDiv = this._renderTranslation( extEndSeq, i, blockStart, blockEnd, blockLength, scale ); frameDiv[frame] = translatedDiv; domClass.add( translatedDiv, "frame" + frame ); } for( var i = 2; i >= 0; i-- ) { block.domNode.appendChild( frameDiv[i] ); } } // make a table to contain the sequences var charSize = this.getCharacterMeasurements('sequence'); var bigTiles = scale > charSize.w + 4; // whether to add .big styles to the base tiles var seqNode; if( this.config.showReverseStrand || this.config.showForwardStrand ) seqNode = dom.create( "table", { className: "sequence" + (bigTiles ? ' big' : '') + (this.config.showColor ? '' : ' nocolor'), style: { width: "100%" } }, block.domNode); // add a table for the forward strand if( this.config.showForwardStrand ) seqNode.appendChild( this._renderSeqTr( blockStart, blockEnd, blockSeq, scale )); // and one for the reverse strand if( this.config.showReverseStrand ) { var comp = this._renderSeqTr( blockStart, blockEnd, Util.complement(blockSeq), scale ); comp.className = 'revcom'; seqNode.appendChild( comp ); if( this.config.showTranslation ) { var frameDiv = []; for(var i = 0; i < 3; i++) { var transStart = blockStart + 1 - i; var frame = (transStart % 3 + 3 + leftover) % 3; var translatedDiv = this._renderTranslation( extStartSeq, i, blockStart, blockEnd, blockLength, scale, true ); frameDiv[frame] = translatedDiv; domClass.add( translatedDiv, "frame" + frame ); } for( var i = 0; i < 3; i++ ) { block.domNode.appendChild( frameDiv[i] ); } } } var totalHeight = 0; array.forEach( block.domNode.childNodes, function( table ) { totalHeight += (table.clientHeight || table.offsetHeight); }); this.heightUpdate( totalHeight, blockIndex ); }, _renderTranslation: function( seq, offset, blockStart, blockEnd, blockLength, scale, reverse ) { seq = reverse ? Util.revcom( seq ) : seq; var extraBases = (seq.length - offset) % 3; var seqSliced = seq.slice( offset, seq.length - extraBases ); var translated = ""; for( var i = 0; i < seqSliced.length; i += 3 ) { var nextCodon = seqSliced.slice(i, i + 3); var aminoAcid = this._codonTable[nextCodon] || this.nbsp; translated += aminoAcid; } translated = reverse ? translated.split("").reverse().join("") : translated; // Flip the translated seq for left-to-right rendering var orientedSeqSliced = reverse ? seqSliced.split("").reverse().join("") : seqSliced var charSize = this.getCharacterMeasurements("aminoAcid"); var bigTiles = scale > charSize.w + 4; // whether to add .big styles to the base tiles var charWidth = 100/(blockLength / 3); var container = dom.create( 'div',{ className: 'translatedSequence' } ); var table = dom.create('table', { className: 'translatedSequence offset'+offset+(bigTiles ? ' big' : ''), style: { width: (charWidth * translated.length) + "%" } }, container ); var tr = dom.create('tr', {}, table ); table.style.left = ( reverse ? 100 - charWidth * (translated.length + offset / 3) : charWidth*offset/3 ) + "%"; charWidth = 100/ translated.length + "%"; var drawChars = scale >= charSize.w; if( drawChars ) table.className += ' big'; for( var i=0; i<translated.length; i++ ) { var aminoAcidSpan = document.createElement('td'); var originalCodon = orientedSeqSliced.slice(3 * i, 3 * i + 3) originalCodon = reverse ? originalCodon.split("").reverse().join("") : originalCodon; aminoAcidSpan.className = 'aminoAcid aminoAcid_'+translated.charAt(i).toLowerCase(); // However, if it's known to be a start/stop, apply those CSS classes instead. if (this._codonStarts.indexOf(originalCodon.toUpperCase()) != -1) { aminoAcidSpan.className = 'aminoAcid aminoAcid_start' } if (this._codonStops.indexOf(originalCodon.toUpperCase()) != -1) { aminoAcidSpan.className = 'aminoAcid aminoAcid_stop' } aminoAcidSpan.style.width = charWidth; if( drawChars ) { aminoAcidSpan.innerHTML = translated.charAt( i ); } tr.appendChild(aminoAcidSpan); } return container; }, /** * Given the start and end coordinates, and the sequence bases, * makes a table row containing the sequence. * @private */ _renderSeqTr: function ( start, end, seq, scale ) { var charSize = this.getCharacterMeasurements('sequence'); var container = document.createElement('tr'); var charWidth = 100/(end-start)+"%"; var drawChars = scale >= charSize.w; var baseClassDefault = 'base'; if(this.config.seqType === 'protein'){ baseClassDefault += ' aaScheme_' + this.config.proteinColorScheme; } for( var i=0; i<seq.length; i++ ) { var base = document.createElement('td'); base.className = baseClassDefault + ' base_'+seq.charAt(i).toLowerCase(); base.style.width = charWidth; if( drawChars ) { base.innerHTML = seq.charAt(i); } container.appendChild(base); } return container; }, startZoom: function() { query('.base', this.div ).empty(); }, /** * @returns {Object} containing <code>h</code> and <code>w</code>, * in pixels, of the characters being used for sequences */ getCharacterMeasurements: function( className ) { return this._charMeasurements[className] || ( this._charMeasurements[className] = this._measureSequenceCharacterSize( this.div, className ) ); }, /** * Conducts a test with DOM elements to measure sequence text width * and height. */ _measureSequenceCharacterSize: function( containerElement, className ) { var widthTest = document.createElement("td"); widthTest.className = className; widthTest.style.visibility = "hidden"; var widthText = "12345678901234567890123456789012345678901234567890"; widthTest.appendChild(document.createTextNode(widthText)); containerElement.appendChild(widthTest); var result = { w: (widthTest.clientWidth / widthText.length)+1, h: widthTest.clientHeight }; containerElement.removeChild(widthTest); return result; }, _trackMenuOptions: function() { var track = this; var o = this.inherited(arguments); o.push( { type: 'dijit/MenuSeparator' } ); o.push.apply( o, [ { label: 'Show forward strand', type: 'dijit/CheckedMenuItem', checked: !! this.config.showForwardStrand, onClick: function(event) { track.config.showForwardStrand = this.checked; track.changed(); } }, { label: 'Show reverse strand', type: 'dijit/CheckedMenuItem', checked: !! this.config.showReverseStrand, onClick: function(event) { track.config.showReverseStrand = this.checked; track.changed(); } }, { label: 'Show translation', type: 'dijit/CheckedMenuItem', checked: !! this.config.showTranslation, onClick: function(event) { track.config.showTranslation = this.checked; track.changed(); } }, { label: 'Show color', type: 'dijit/CheckedMenuItem', checked: !! this.config.showColor, onClick: function(event) { track.config.showColor = this.checked; track.changed(); } } ]); return o; } }); });