@gmod/jbrowse
Version:
JBrowse - client-side genome browser
334 lines (289 loc) • 15.4 kB
JavaScript
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;
}
});
});