/** * 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( $("").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 }; });