UNPKG

@gmod/jbrowse

Version:

JBrowse - client-side genome browser

334 lines (289 loc) 15.4 kB
define( [ 'dojo/_base/declare', 'dojo/_base/array', 'dojo/_base/lang', 'dojo/aspect', 'dojo/on', 'JBrowse/has', 'dojo/window', 'dojo/dom-construct', 'JBrowse/Util', 'dijit/form/TextBox', 'dijit/form/Button', 'dijit/form/RadioButton', 'dijit/Dialog', 'FileSaver/FileSaver' ], function( declare, array, lang, aspect, on, has, dojoWindow, dom, Util, dijitTextBox, dijitButton, dijitRadioButton, dijitDialog, FileSaver ) { /** * Mixin for a track that can export its data. * @lends JBrowse.View.Track.ExportMixin */ return declare( null, { _canSaveFiles: function() { return has('save-generated-files') && ! this.config.noExportFiles; }, _canExport: function() { if( this.config.noExport ) return false; var highlightedRegion = this.browser.getHighlight(); var visibleRegion = this.browser.view.visibleRegion(); var wholeRefSeqRegion = { ref: this.refSeq.name, start: this.refSeq.start, end: this.refSeq.end }; var canExportVisibleRegion = this._canExportRegion( visibleRegion ); var canExportWholeRef = this._canExportRegion( wholeRefSeqRegion ); return highlightedRegion && this._canExportRegion( highlightedRegion ) || this._canExportRegion( visibleRegion ) || this._canExportRegion( wholeRefSeqRegion ); }, _possibleExportRegions: function() { var regions = [ // the visible region (function() { var r = dojo.clone( this.browser.view.visibleRegion() ); r.description = 'Visible region'; r.name = 'visible'; return r; }.call(this)), // whole reference sequence { ref: this.refSeq.name, start: this.refSeq.start, end: this.refSeq.end, description: 'Whole reference sequence', name: 'wholeref' } ]; var highlightedRegion = this.browser.getHighlight(); if( highlightedRegion ) { regions.unshift( lang.mixin( lang.clone( highlightedRegion ), { description: "Highlighted region", name: "highlight" } ) ); } return regions; }, _exportDialogContent: function() { // note that the `this` for this content function is not the track, it's the menu-rendering context var possibleRegions = this.track._possibleExportRegions(); // for each region, calculate its length and determine whether we can export it array.forEach( possibleRegions, function( region ) { region.length = Math.round( region.end - region.start + 1 ); region.canExport = this._canExportRegion( region ); },this.track); var setFilenameValue = dojo.hitch(this.track, function() { var region = this._readRadio(form.elements.region); var format = nameToExtension[this._readRadio(form.elements.format)]; form.elements.filename.value = ((this.key || this.label) + "-" + region).replace(/[^ .a-zA-Z0-9_-]/g,'-') + "." + format; }); var form = dom.create('form', { onSubmit: function() { return false; } }); var regionFieldset = dom.create('fieldset', {className: "region"}, form ); dom.create('legend', {innerHTML: "Region to save"}, regionFieldset); var checked = 0; array.forEach( possibleRegions, function(r) { var locstring = Util.assembleLocString(r); var regionButton = new dijitRadioButton( { name: "region", id: "region_"+r.name, value: locstring, checked: r.canExport && !(checked++) ? "checked" : "" }); regionFieldset.appendChild(regionButton.domNode); var regionButtonLabel = dom.create("label", {"for": regionButton.id, innerHTML: r.description+' - <span class="locString">' + locstring+'</span> ('+Util.humanReadableNumber(r.length)+(r.canExport ? 'b' : 'b, too large')+')'}, regionFieldset); if(!r.canExport) { regionButton.domNode.disabled = "disabled"; regionButtonLabel.className = "ghosted"; } on(regionButton, "click", setFilenameValue); dom.create('br',{},regionFieldset); }); var formatFieldset = dom.create("fieldset", {className: "format"}, form); dom.create("legend", {innerHTML: "Format"}, formatFieldset); checked = 0; var nameToExtension = {}; array.forEach( this.track._exportFormats(), function(fmt) { if( ! fmt.name ) { fmt = { name: fmt, label: fmt }; } if( ! fmt.fileExt) { fmt.fileExt = fmt.name || fmt; } nameToExtension[fmt.name] = fmt.fileExt; var formatButton = new dijitRadioButton({ name: "format", id: "format"+fmt.name, value: fmt.name, checked: checked++?"":"checked"}); formatFieldset.appendChild(formatButton.domNode); var formatButtonLabel = dom.create("label", {"for": formatButton.id, innerHTML: fmt.label}, formatFieldset); on(formatButton, "click", setFilenameValue); dom.create( "br", {}, formatFieldset ); },this); var filenameFieldset = dom.create("fieldset", {className: "filename"}, form); dom.create("legend", {innerHTML: "Filename"}, filenameFieldset); dom.create("input", {type: "text", name: "filename", style: {width: "100%"}}, filenameFieldset); setFilenameValue(); var actionBar = dom.create( 'div', { className: 'dijitDialogPaneActionBar' }); // note that the `this` for this content function is not the track, it's the menu-rendering context var dialog = this.dialog; new dijitButton({ iconClass: 'dijitIconDelete', onClick: dojo.hitch(dialog,'hide'), label: 'Cancel' }) .placeAt( actionBar ); var viewButton = new dijitButton({ iconClass: 'dijitIconTask', label: 'View', disabled: ! array.some(possibleRegions,function(r) { return r.canExport; }), onClick: lang.partial( this.track._exportViewButtonClicked, this.track, form, dialog ) }) .placeAt( actionBar ); // don't show a download button if we for some reason can't save files if( this.track._canSaveFiles() ) { var dlButton = new dijitButton({ iconClass: 'dijitIconSave', label: 'Save', disabled: ! array.some(possibleRegions,function(r) { return r.canExport; }), onClick: dojo.hitch( this.track, function() { var format = this._readRadio( form.elements.format ); var region = this._readRadio( form.elements.region ); var filename = form.elements.filename.value.replace(/[^ .a-zA-Z0-9_-]/g,'-'); dlButton.set('disabled',true); dlButton.set('iconClass','jbrowseIconBusy'); this.exportRegion( region, format, dojo.hitch( this, function( output ) { dialog.hide(); this._fileDownload({ format: format, data: output, filename: filename }); })); })}) .placeAt( actionBar ); } return [ form, actionBar ]; }, // run when the 'View' button is clicked in the export dialog _exportViewButtonClicked: function( track, form, dialog ) { var viewButton = this; viewButton.set('disabled',true); viewButton.set('iconClass','jbrowseIconBusy'); var region = track._readRadio( form.elements.region ); var format = track._readRadio( form.elements.format ); var filename = form.elements.filename.value.replace(/[^ .a-zA-Z0-9_-]/g,'-'); track.exportRegion( region, format, function(output) { dialog.hide(); var text = dom.create('textarea', { rows: Math.round( dojoWindow.getBox().h / 12 * 0.5 ), wrap: 'off', cols: 80, style: "maxWidth: 90em; overflow: scroll; overflow-y: scroll; overflow-x: scroll; overflow:-moz-scrollbars-vertical;", readonly: true }); text.value = output; var actionBar = dom.create( 'div', { className: 'dijitDialogPaneActionBar' }); var exportView = new dijitDialog({ className: 'export-view-dialog', title: format + ' export - <span class="locString">'+ region+'</span> ('+Util.humanReadableNumber(output.length)+'bytes)', content: [ text, actionBar ] }); new dijitButton({ iconClass: 'dijitIconDelete', label: 'Close', onClick: dojo.hitch( exportView, 'hide' ) }) .placeAt(actionBar); // only show a button if the browser can save files if( track._canSaveFiles() ) { var saveDiv = dom.create( "div", { className: "save" }, actionBar ); var saveButton = new dijitButton( { iconClass: 'dijitIconSave', label: 'Save', onClick: function() { var filename = fileNameText.get('value').replace(/[^ .a-zA-Z0-9_-]/g,'-'); exportView.hide(); track._fileDownload({ format: format, data: output, filename: filename }); } }).placeAt(saveDiv); var fileNameText = new dijitTextBox({ value: filename, style: "width: 24em" }).placeAt( saveDiv ); } aspect.after( exportView, 'hide', function() { // manually unhook and free the (possibly huge) text area text.parentNode.removeChild( text ); text = null; setTimeout( function() { exportView.destroyRecursive(); }, 500 ); }); exportView.show(); }); }, _fileDownload: function( args ) { FileSaver.saveAs(new Blob([args.data], {type: args.format ? 'application/x-'+args.format.toLowerCase() : 'text/plain'}), args.filename); // We will need to check whether this breaks the WebApollo plugin. }, // cross-platform function for (portably) reading the value of a radio control. sigh. *rolls eyes* _readRadio: function( r ) { if( r.length ) { for( var i = 0; i<r.length; i++ ) { if( r[i].checked ) return r[i].value; } } return r.value; }, exportRegion: function( region, format, callback ) { // parse the locstring if necessary if( typeof region == 'string' ) region = Util.parseLocString( region ); // we can only export from the currently-visible reference // sequence right now if( region.ref != this.refSeq.name ) { console.error("cannot export data for ref seq "+region.ref+", " + "exporting is currently only supported for the " + "currently-visible reference sequence" ); return; } dojo.global.require( [format.match(/\//)?format:'JBrowse/View/Export/'+format], dojo.hitch(this,function( exportDriver ) { new exportDriver({ refSeq: this.refSeq, track: this, store: this.store }).exportRegion( region, callback ); })); }, _trackMenuOptions: function() { var opts = this.inherited(arguments); if( ! this.config.noExport ) // add a "Save track data as" option to the track menu opts.push({ label: 'Save track data', iconClass: 'dijitIconSave', disabled: ! this._canExport(), action: 'bareDialog', content: this._exportDialogContent, dialog: { id: 'exportDialog', className: 'export-dialog' } }); return opts; }, _canExportRegion: function( l ) { //console.log('can generic export?'); if( ! l ) return false; // if we have a maxExportSpan configured for this track, use it. if( typeof this.config.maxExportSpan == 'number' || typeof this.config.maxExportSpan == 'string' ) { return l.end - l.start + 1 <= this.config.maxExportSpan; } else { // if we know the store's feature density, then use that with // a limit of maxExportFeatures or 5,000 features var thisB = this; var storeStats = {}; // will return immediately if the stats are available this.store.getGlobalStats( function( s ) { storeStats = s; }, function(error){ }); // error callback does nothing for now if( storeStats.featureDensity ) { return storeStats.featureDensity*(l.end - l.start) <= ( thisB.config.maxExportFeatures || 50000 ); } } // otherwise, i guess we can export return true; } }); });