Files
galaxy/static/scripts/mvc/tools.js
T

764 lines
21 KiB
JavaScript

/**
* Model, view, and controller objects for Galaxy tools and tool panel.
*/
define( ["libs/underscore", "viz/trackster/util", "mvc/data" ],
function(_, util, data) {
/**
* Mixin for tracking model visibility.
*/
var VisibilityMixin = {
hidden: false,
show: function() {
this.set("hidden", false);
},
hide: function() {
this.set("hidden", true);
},
toggle: function() {
this.set("hidden", !this.get("hidden"));
},
is_visible: function() {
return !this.attributes.hidden;
}
};
/**
* A tool parameter.
*/
var ToolParameter = Backbone.Model.extend({
defaults: {
name: null,
label: null,
type: null,
value: null,
html: null,
num_samples: 5
},
initialize: function(options) {
this.attributes.html = unescape(this.attributes.html);
},
copy: function() {
return new ToolParameter(this.toJSON());
},
set_value: function(value) {
this.set('value', value || '');
}
});
var ToolParameterCollection = Backbone.Collection.extend({
model: ToolParameter
});
/**
* A data tool parameter.
*/
var DataToolParameter = ToolParameter.extend({});
/**
* An integer tool parameter.
*/
var IntegerToolParameter = ToolParameter.extend({
set_value: function(value) {
this.set('value', parseInt(value, 10));
},
/**
* Returns samples from a tool input.
*/
get_samples: function() {
return d3.scale.linear()
.domain([this.get('min'), this.get('max')])
.ticks(this.get('num_samples'));
}
});
var FloatToolParameter = IntegerToolParameter.extend({
set_value: function(value) {
this.set('value', parseFloat(value));
}
});
/**
* A select tool parameter.
*/
var SelectToolParameter = ToolParameter.extend({
/**
* Returns tool options.
*/
get_samples: function() {
return _.map(this.get('options'), function(option) {
return option[0];
});
}
});
// Set up dictionary of parameter types.
ToolParameter.subModelTypes = {
'integer': IntegerToolParameter,
'float': FloatToolParameter,
'data': DataToolParameter,
'select': SelectToolParameter
};
/**
* A Galaxy tool.
*/
var Tool = Backbone.Model.extend({
// Default attributes.
defaults: {
id: null,
name: null,
description: null,
target: null,
inputs: [],
outputs: []
},
urlRoot: galaxy_config.root + 'api/tools',
initialize: function(options) {
// Set parameters.
this.set('inputs', new ToolParameterCollection(_.map(options.inputs, function(p) {
var p_class = ToolParameter.subModelTypes[p.type] || ToolParameter;
return new p_class(p);
})));
},
/**
*
*/
toJSON: function() {
var rval = Backbone.Model.prototype.toJSON.call(this);
// Convert inputs to JSON manually.
rval.inputs = this.get('inputs').map(function(i) { return i.toJSON(); });
return rval;
},
/**
* Removes inputs of a particular type; this is useful because not all inputs can be handled by
* client and server yet.
*/
remove_inputs: function(types) {
var tool = this,
incompatible_inputs = tool.get('inputs').filter( function(input) {
return ( types.indexOf( input.get('type') ) !== -1);
});
tool.get('inputs').remove(incompatible_inputs);
},
/**
* Returns object copy, optionally including only inputs that can be sampled.
*/
copy: function(only_samplable_inputs) {
var copy = new Tool(this.toJSON());
// Return only samplable inputs if flag is set.
if (only_samplable_inputs) {
var valid_inputs = new Backbone.Collection();
copy.get('inputs').each(function(input) {
if (input.get_samples()) {
valid_inputs.push(input);
}
});
copy.set('inputs', valid_inputs);
}
return copy;
},
apply_search_results: function(results) {
( _.indexOf(results, this.attributes.id) !== -1 ? this.show() : this.hide() );
return this.is_visible();
},
/**
* Set a tool input's value.
*/
set_input_value: function(name, value) {
this.get('inputs').find(function(input) {
return input.get('name') === name;
}).set('value', value);
},
/**
* Set many input values at once.
*/
set_input_values: function(inputs_dict) {
var self = this;
_.each(_.keys(inputs_dict), function(input_name) {
self.set_input_value(input_name, inputs_dict[input_name]);
});
},
/**
* Run tool; returns a Deferred that resolves to the tool's output(s).
*/
run: function() {
return this._run();
},
/**
* Rerun tool using regions and a target dataset.
*/
rerun: function(target_dataset, regions) {
return this._run({
action: 'rerun',
target_dataset_id: target_dataset.id,
regions: regions
});
},
/**
* Returns input dict for tool's inputs.
*/
get_inputs_dict: function() {
var input_dict = {};
this.get('inputs').each(function(input) {
input_dict[input.get('name')] = input.get('value');
});
return input_dict;
},
/**
* Run tool; returns a Deferred that resolves to the tool's output(s).
* NOTE: this method is a helper method and should not be called directly.
*/
_run: function(additional_params) {
// Create payload.
var payload = _.extend({
tool_id: this.id,
inputs: this.get_inputs_dict()
}, additional_params);
// Because job may require indexing datasets, use server-side
// deferred to ensure that job is run. Also use deferred that
// resolves to outputs from tool.
var run_deferred = $.Deferred(),
ss_deferred = new util.ServerStateDeferred({
ajax_settings: {
url: this.urlRoot,
data: JSON.stringify(payload),
dataType: "json",
contentType: 'application/json',
type: "POST"
},
interval: 2000,
success_fn: function(response) {
return response !== "pending";
}
});
// Run job and resolve run_deferred to tool outputs.
$.when(ss_deferred.go()).then(function(result) {
run_deferred.resolve(new data.DatasetCollection().reset(result));
});
return run_deferred;
}
});
_.extend(Tool.prototype, VisibilityMixin);
/**
* Tool view.
*/
var ToolView = Backbone.View.extend({
});
/**
* Wrap collection of tools for fast access/manipulation.
*/
var ToolCollection = Backbone.Collection.extend({
model: Tool
});
/**
* Label or section header in tool panel.
*/
var ToolSectionLabel = Backbone.Model.extend(VisibilityMixin);
/**
* Section of tool panel with elements (labels and tools).
*/
var ToolSection = Backbone.Model.extend({
defaults: {
elems: [],
open: false
},
clear_search_results: function() {
_.each(this.attributes.elems, function(elt) {
elt.show();
});
this.show();
this.set("open", false);
},
apply_search_results: function(results) {
var all_hidden = true,
cur_label;
_.each(this.attributes.elems, function(elt) {
if (elt instanceof ToolSectionLabel) {
cur_label = elt;
cur_label.hide();
}
else if (elt instanceof Tool) {
if (elt.apply_search_results(results)) {
all_hidden = false;
if (cur_label) {
cur_label.show();
}
}
}
});
if (all_hidden) {
this.hide();
}
else {
this.show();
this.set("open", true);
}
}
});
_.extend(ToolSection.prototype, VisibilityMixin);
/**
* Tool search that updates results when query is changed. Result value of null
* indicates that query was not run; if not null, results are from search using
* query.
*/
var ToolSearch = Backbone.Model.extend({
defaults: {
search_hint_string: "search tools",
min_chars_for_search: 3,
spinner_url: "",
clear_btn_url: "",
search_url: "",
visible: true,
query: "",
results: null,
// ESC (27) will clear the input field and tool search filters
clear_key: 27
},
initialize: function() {
this.on("change:query", this.do_search);
},
/**
* Do the search and update the results.
*/
do_search: function() {
var query = this.attributes.query;
// If query is too short, do not search.
if (query.length < this.attributes.min_chars_for_search) {
this.set("results", null);
return;
}
// Do search via AJAX.
var q = query + '*';
// Stop previous ajax-request
if (this.timer) {
clearTimeout(this.timer);
}
// Start a new ajax-request in X ms
$("#search-clear-btn").hide();
$("#search-spinner").show();
var self = this;
this.timer = setTimeout(function () {
$.get(self.attributes.search_url, { query: q }, function (data) {
self.set("results", data);
$("#search-spinner").hide();
$("#search-clear-btn").show();
}, "json" );
}, 200 );
},
clear_search: function() {
this.set("query", "");
this.set("results", null);
}
});
_.extend(ToolSearch.prototype, VisibilityMixin);
/**
* Tool Panel.
*/
var ToolPanel = Backbone.Model.extend({
initialize: function(options) {
this.attributes.tool_search = options.tool_search;
this.attributes.tool_search.on("change:results", this.apply_search_results, this);
this.attributes.tools = options.tools;
this.attributes.layout = new Backbone.Collection( this.parse(options.layout) );
},
/**
* Parse tool panel dictionary and return collection of tool panel elements.
*/
parse: function(response) {
// Recursive function to parse tool panel elements.
var self = this,
// Helper to recursively parse tool panel.
parse_elt = function(elt_dict) {
var type = elt_dict.model_class;
// There are many types of tools; for now, anything that ends in 'Tool'
// is treated as a generic tool.
if ( type.indexOf('Tool') === type.length - 4 ) {
return self.attributes.tools.get(elt_dict.id);
}
else if (type === 'ToolSection') {
// Parse elements.
var elems = _.map(elt_dict.elems, parse_elt);
elt_dict.elems = elems;
return new ToolSection(elt_dict);
}
else if (type === 'ToolSectionLabel') {
return new ToolSectionLabel(elt_dict);
}
};
return _.map(response, parse_elt);
},
clear_search_results: function() {
this.get('layout').each(function(panel_elt) {
if (panel_elt instanceof ToolSection) {
panel_elt.clear_search_results();
}
else {
// Label or tool, so just show.
panel_elt.show();
}
});
},
apply_search_results: function() {
var results = this.get('tool_search').get('results');
if (results === null) {
this.clear_search_results();
return;
}
var cur_label = null;
this.get('layout').each(function(panel_elt) {
if (panel_elt instanceof ToolSectionLabel) {
cur_label = panel_elt;
cur_label.hide();
}
else if (panel_elt instanceof Tool) {
if (panel_elt.apply_search_results(results)) {
if (cur_label) {
cur_label.show();
}
}
}
else {
// Starting new section, so clear current label.
cur_label = null;
panel_elt.apply_search_results(results);
}
});
}
});
/**
* View classes for Galaxy tools and tool panel.
*
* Views use precompiled Handlebars templates for rendering. Views update as needed
* based on (a) model/collection events and (b) user interactions; in this sense,
* they are controllers are well and the HTML is the real view in the MVC architecture.
*/
/**
* Base view that handles visibility based on model's hidden attribute.
*/
var BaseView = Backbone.View.extend({
initialize: function() {
this.model.on("change:hidden", this.update_visible, this);
this.update_visible();
},
update_visible: function() {
( this.model.attributes.hidden ? this.$el.hide() : this.$el.show() );
}
});
/**
* Link to a tool.
*/
var ToolLinkView = BaseView.extend({
tagName: 'div',
render: function() {
this.$el.append( Handlebars.templates.tool_link(this.model.toJSON()) );
return this;
}
});
/**
* Panel label/section header.
*/
var ToolSectionLabelView = BaseView.extend({
tagName: 'div',
className: 'toolPanelLabel',
render: function() {
this.$el.append( $("<span/>").text(this.model.attributes.text) );
return this;
}
});
/**
* Panel section.
*/
var ToolSectionView = BaseView.extend({
tagName: 'div',
className: 'toolSectionWrapper',
initialize: function() {
BaseView.prototype.initialize.call(this);
this.model.on("change:open", this.update_open, this);
},
render: function() {
// Build using template.
this.$el.append( Handlebars.templates.panel_section(this.model.toJSON()) );
// Add tools to section.
var section_body = this.$el.find(".toolSectionBody");
_.each(this.model.attributes.elems, function(elt) {
if (elt instanceof Tool) {
var tool_view = new ToolLinkView({model: elt, className: "toolTitle"});
tool_view.render();
section_body.append(tool_view.$el);
}
else if (elt instanceof ToolSectionLabel) {
var label_view = new ToolSectionLabelView({model: elt});
label_view.render();
section_body.append(label_view.$el);
}
else {
// TODO: handle nested section bodies?
}
});
return this;
},
events: {
'click .toolSectionTitle > a': 'toggle'
},
/**
* Toggle visibility of tool section.
*/
toggle: function() {
this.model.set("open", !this.model.attributes.open);
},
/**
* Update whether section is open or close.
*/
update_open: function() {
(this.model.attributes.open ?
this.$el.children(".toolSectionBody").slideDown("fast") :
this.$el.children(".toolSectionBody").slideUp("fast")
);
}
});
var ToolSearchView = Backbone.View.extend({
tagName: 'div',
id: 'tool-search',
className: 'bar',
events: {
'click': 'focus_and_select',
'keyup :input': 'query_changed',
'click #search-clear-btn': 'clear'
},
render: function() {
this.$el.append( Handlebars.templates.tool_search(this.model.toJSON()) );
if (!this.model.is_visible()) {
this.$el.hide();
}
this.$el.find('[title]').tooltip();
return this;
},
focus_and_select: function() {
this.$el.find(":input").focus().select();
},
clear: function() {
this.model.clear_search();
this.$el.find(":input").val(this.model.attributes.search_hint_string);
this.focus_and_select();
return false;
},
query_changed: function( evData ) {
// check for the 'clear key' (ESC) first
if( ( this.model.attributes.clear_key ) &&
( this.model.attributes.clear_key === evData.which ) ){
this.clear();
return false;
}
this.model.set("query", this.$el.find(":input").val());
}
});
/**
* Tool panel view. Events triggered include:
* tool_link_click(click event, tool_model)
*/
var ToolPanelView = Backbone.View.extend({
tagName: 'div',
className: 'toolMenu',
/**
* Set up view.
*/
initialize: function() {
this.model.get('tool_search').on("change:results", this.handle_search_results, this);
},
render: function() {
var self = this;
// Render search.
var search_view = new ToolSearchView( { model: this.model.get('tool_search') } );
search_view.render();
self.$el.append(search_view.$el);
// Render panel.
this.model.get('layout').each(function(panel_elt) {
if (panel_elt instanceof ToolSection) {
var section_title_view = new ToolSectionView({model: panel_elt});
section_title_view.render();
self.$el.append(section_title_view.$el);
}
else if (panel_elt instanceof Tool) {
var tool_view = new ToolLinkView({model: panel_elt, className: "toolTitleNoSection"});
tool_view.render();
self.$el.append(tool_view.$el);
}
else if (panel_elt instanceof ToolSectionLabel) {
var label_view = new ToolSectionLabelView({model: panel_elt});
label_view.render();
self.$el.append(label_view.$el);
}
});
// Setup tool link click eventing.
self.$el.find("a.tool-link").click(function(e) {
// Tool id is always the first class.
var
tool_id = $(this).attr('class').split(/\s+/)[0],
tool = self.model.get('tools').get(tool_id);
self.trigger("tool_link_click", e, tool);
});
return this;
},
handle_search_results: function() {
var results = this.model.get('tool_search').get('results');
if (results && results.length === 0) {
$("#search-no-results").show();
}
else {
$("#search-no-results").hide();
}
}
});
/**
* View for working with a tool: setting parameters and inputs and executing the tool.
*/
var ToolFormView = Backbone.View.extend({
className: 'toolForm',
render: function() {
this.$el.children().remove();
this.$el.append( Handlebars.templates.tool_form(this.model.toJSON()) );
}
});
/**
* Integrated tool menu + tool execution.
*/
var IntegratedToolMenuAndView = Backbone.View.extend({
className: 'toolMenuAndView',
initialize: function() {
this.tool_panel_view = new ToolPanelView({collection: this.collection});
this.tool_form_view = new ToolFormView();
},
render: function() {
// Render and append tool panel.
this.tool_panel_view.render();
this.tool_panel_view.$el.css("float", "left");
this.$el.append(this.tool_panel_view.$el);
// Append tool form view.
this.tool_form_view.$el.hide();
this.$el.append(this.tool_form_view.$el);
// On tool link click, show tool.
var self = this;
this.tool_panel_view.on("tool_link_click", function(e, tool) {
// Prevents click from activating link:
e.preventDefault();
// Show tool that was clicked on:
self.show_tool(tool);
});
},
/**
* Fetch and display tool.
*/
show_tool: function(tool) {
var self = this;
tool.fetch().done( function() {
self.tool_form_view.model = tool;
self.tool_form_view.render();
self.tool_form_view.$el.show();
$('#left').width("650px");
});
}
});
// Exports
return {
ToolParameter: ToolParameter,
IntegerToolParameter: IntegerToolParameter,
SelectToolParameter: SelectToolParameter,
Tool: Tool,
ToolCollection: ToolCollection,
ToolSearch: ToolSearch,
ToolPanel: ToolPanel,
ToolPanelView: ToolPanelView,
ToolFormView: ToolFormView
};
});