/static/scripts/galaxy.workflow_editor.canvas.js
JavaScript | 1973 lines | 1710 code | 130 blank | 133 comment | 237 complexity | b2bb68ccfa5d7806903e0e72880ea568 MD5 | raw file
Possible License(s): CC-BY-3.0
- function CollectionTypeDescription( collectionType ) {
- this.collectionType = collectionType;
- this.isCollection = true;
- this.rank = collectionType.split(":").length;
- }
- $.extend( CollectionTypeDescription.prototype, {
- append: function( otherCollectionTypeDescription ) {
- if( otherCollectionTypeDescription === NULL_COLLECTION_TYPE_DESCRIPTION ) {
- return this;
- }
- if( otherCollectionTypeDescription === ANY_COLLECTION_TYPE_DESCRIPTION ) {
- return otherCollectionType;
- }
- return new CollectionTypeDescription( this.collectionType + ":" + otherCollectionTypeDescription.collectionType );
- },
- canMatch: function( otherCollectionTypeDescription ) {
- if( otherCollectionTypeDescription === NULL_COLLECTION_TYPE_DESCRIPTION ) {
- return false;
- }
- if( otherCollectionTypeDescription === ANY_COLLECTION_TYPE_DESCRIPTION ) {
- return true;
- }
- return otherCollectionTypeDescription.collectionType == this.collectionType;
- },
- canMapOver: function( otherCollectionTypeDescription ) {
- if( otherCollectionTypeDescription === NULL_COLLECTION_TYPE_DESCRIPTION ) {
- return false;
- }
- if( otherCollectionTypeDescription === ANY_COLLECTION_TYPE_DESCRIPTION ) {
- return false;
- }
- if( this.rank <= otherCollectionTypeDescription.rank ) {
- // Cannot map over self...
- return false;
- }
- var requiredSuffix = otherCollectionTypeDescription.collectionType
- return this._endsWith( this.collectionType, requiredSuffix );
- },
- effectiveMapOver: function( otherCollectionTypeDescription ) {
- var otherCollectionType = otherCollectionTypeDescription.collectionType;
- var effectiveCollectionType = this.collectionType.substring( 0, this.collectionType.length - otherCollectionType.length - 1 );
- return new CollectionTypeDescription( effectiveCollectionType );
- },
- equal: function( otherCollectionTypeDescription ) {
- return otherCollectionTypeDescription.collectionType == this.collectionType;
- },
- toString: function() {
- return "CollectionType[" + this.collectionType + "]";
- },
- _endsWith: function( str, suffix ) {
- return str.indexOf(suffix, str.length - suffix.length) !== -1;
- }
- } );
- NULL_COLLECTION_TYPE_DESCRIPTION = {
- isCollection: false,
- canMatch: function( other ) { return false; },
- canMapOver: function( other ) {
- return false;
- },
- toString: function() {
- return "NullCollectionType[]";
- },
- append: function( otherCollectionType ) {
- return otherCollectionType;
- },
- equal: function( other ) {
- return other === this;
- }
- };
- ANY_COLLECTION_TYPE_DESCRIPTION = {
- isCollection: true,
- canMatch: function( other ) { return NULL_COLLECTION_TYPE_DESCRIPTION !== other; },
- canMapOver: function( other ) {
- return false;
- },
- toString: function() {
- return "AnyCollectionType[]";
- },
- append: function( otherCollectionType ) {
- throw "Cannot append to ANY_COLLECTION_TYPE_DESCRIPTION";
- },
- equal: function( other ) {
- return other === this;
- }
- };
- var TerminalMapping = Backbone.Model.extend( {
- initialize: function( attr ) {
- this.mapOver = attr.mapOver || NULL_COLLECTION_TYPE_DESCRIPTION;
- this.terminal = attr.terminal;
- this.terminal.terminalMapping = this;
- },
- disableMapOver: function() {
- this.setMapOver( NULL_COLLECTION_TYPE_DESCRIPTION );
- },
- setMapOver: function( collectionTypeDescription ) {
- // TODO: Can I use "attributes" or something to auto trigger "change"
- // event?
- this.mapOver = collectionTypeDescription;
- this.trigger("change");
- }
- } );
- var TerminalMappingView = Backbone.View.extend( {
- tagName: "div",
- className: "fa-icon-button fa fa-folder-o",
- initialize: function( options ) {
- var mapText = "Run tool in parallel over collection";
- this.$el.tooltip( {delay: 500, title: mapText } );
- this.model.bind( "change", _.bind( this.render, this ) );
- },
- render: function() {
- if( this.model.mapOver.isCollection ) {
- this.$el.show();
- } else {
- this.$el.hide();
- }
- },
- } );
- var InputTerminalMappingView = TerminalMappingView.extend( {
- events: {
- "click": "onClick",
- "mouseenter": "onMouseEnter",
- "mouseleave": "onMouseLeave",
- },
- onMouseEnter: function( e ) {
- var model = this.model;
- if( ! model.terminal.connected() && model.mapOver.isCollection ) {
- this.$el.css( "color", "red" );
- }
- },
- onMouseLeave: function( e ) {
- this.$el.css( "color", "black" );
- },
- onClick: function( e ) {
- var model = this.model;
- if( ! model.terminal.connected() && model.mapOver.isCollection ) {
- // TODO: Consider prompting...
- model.terminal.resetMapping();
- }
- },
- } );
- var InputTerminalMapping = TerminalMapping;
- var InputCollectionTerminalMapping = TerminalMapping;
- var OutputTerminalMapping = TerminalMapping;
- var OutputTerminalMappingView = TerminalMappingView;
- var InputCollectionTerminalMappingView = InputTerminalMappingView;
- var OutputCollectionTerminalMapping = TerminalMapping;
- var OutputCollectionTerminalMappingView = TerminalMappingView;
- var Terminal = Backbone.Model.extend( {
- initialize: function( attr ) {
- this.element = attr.element;
- this.connectors = [];
- },
- connect: function ( connector ) {
- this.connectors.push( connector );
- if ( this.node ) {
- this.node.markChanged();
- }
- },
- disconnect: function ( connector ) {
- this.connectors.splice( $.inArray( connector, this.connectors ), 1 );
- if ( this.node ) {
- this.node.markChanged();
- this.resetMappingIfNeeded();
- }
- },
- redraw: function () {
- $.each( this.connectors, function( _, c ) {
- c.redraw();
- });
- },
- destroy: function () {
- $.each( this.connectors.slice(), function( _, c ) {
- c.destroy();
- });
- },
- destroyInvalidConnections: function( ) {
- _.each( this.connectors, function( connector ) {
- connector.destroyIfInvalid();
- } );
- },
- setMapOver : function( val ) {
- if( this.multiple ) {
- return; // Cannot set this to be multirun...
- }
- if( ! this.mapOver().equal( val ) ) {
- this.terminalMapping.setMapOver( val );
- _.each( this.node.output_terminals, function( outputTerminal ) {
- outputTerminal.setMapOver( val );
- } );
- }
- },
- mapOver: function( ) {
- if ( ! this.terminalMapping ) {
- return NULL_COLLECTION_TYPE_DESCRIPTION;
- } else {
- return this.terminalMapping.mapOver;
- }
- },
- isMappedOver: function( ) {
- return this.terminalMapping && this.terminalMapping.mapOver.isCollection;
- },
- resetMapping: function() {
- this.terminalMapping.disableMapOver();
- },
- resetMappingIfNeeded: function( ) {}, // Subclasses should override this...
- } );
- var OutputTerminal = Terminal.extend( {
- initialize: function( attr ) {
- Terminal.prototype.initialize.call( this, attr );
- this.datatypes = attr.datatypes;
- },
- resetMappingIfNeeded: function( ) {
- // If inputs were only mapped over to preserve
- // an output just disconnected reset these...
- if( ! this.node.hasConnectedOutputTerminals() && ! this.node.hasConnectedMappedInputTerminals()){
- _.each( this.node.mappedInputTerminals(), function( mappedInput ) {
- mappedInput.resetMappingIfNeeded();
- } );
- }
- var noMappedInputs = ! this.node.hasMappedOverInputTerminals();
- if( noMappedInputs ) {
- this.resetMapping();
- }
- },
- resetMapping: function() {
- this.terminalMapping.disableMapOver();
- _.each( this.connectors, function( connector ) {
- var connectedInput = connector.handle2;
- if( connectedInput ) {
- // Not exactly right because this is still connected.
- // Either rewrite resetMappingIfNeeded or disconnect
- // and reconnect if valid.
- connectedInput.resetMappingIfNeeded();
- connector.destroyIfInvalid();
- }
- } );
- }
- } );
- var BaseInputTerminal = Terminal.extend( {
- initialize: function( attr ) {
- Terminal.prototype.initialize.call( this, attr );
- this.update( attr.input ); // subclasses should implement this...
- },
- canAccept: function ( other ) {
- if( this._inputFilled() ) {
- return false;
- } else {
- return this.attachable( other );
- }
- },
- resetMappingIfNeeded: function( ) {
- var mapOver = this.mapOver();
- if( ! mapOver.isCollection ) {
- return;
- }
- // No output terminals are counting on this being mapped
- // over if connected inputs are still mapped over or if none
- // of the outputs are connected...
- var reset = this.node.hasConnectedMappedInputTerminals() ||
- ( ! this.node.hasConnectedOutputTerminals() );
- if( reset ) {
- this.resetMapping();
- }
- },
- resetMapping: function() {
- this.terminalMapping.disableMapOver();
- if( ! this.node.hasMappedOverInputTerminals() ) {
- _.each( this.node.output_terminals, function( terminal) {
- // This shouldn't be called if there are mapped over
- // outputs.
- terminal.resetMapping();
- } );
- }
- },
- connected: function() {
- return this.connectors.length !== 0;
- },
- _inputFilled: function() {
- var inputFilled;
- if( ! this.connected() ) {
- inputFilled = false;
- } else {
- if( this.multiple ) {
- if( ! this.connected() ) {
- inputFilled = false;
- } else {
- var firstOutput = this.connectors[ 0 ].handle1;
- if( ! firstOutput ){
- inputFilled = false;
- } else {
- if( firstOutput.isDataCollectionInput || firstOutput.isMappedOver() || firstOutput.datatypes.indexOf( "input_collection" ) > 0 ) {
- inputFilled = true;
- } else {
- inputFilled = false;
- }
- }
- }
- } else {
- inputFilled = true;
- }
- }
- return inputFilled;
- },
- _mappingConstraints: function( ) {
- // If this is a connected terminal, return list of collection types
- // other terminals connected to node are constraining mapping to.
- if( ! this.node ) {
- return []; // No node - completely unconstrained
- }
- var mapOver = this.mapOver();
- if( mapOver.isCollection ) {
- return [ mapOver ];
- }
- var constraints = [];
- if( ! this.node.hasConnectedOutputTerminals() ) {
- _.each( this.node.connectedMappedInputTerminals(), function( inputTerminal ) {
- constraints.push( inputTerminal.mapOver() );
- } );
- } else {
- // All outputs should have same mapOver status - least specific.
- constraints.push( _.first( _.values( this.node.output_terminals ) ).mapOver() );
- }
- return constraints;
- },
- _producesAcceptableDatatype: function( other ) {
- // other is a non-collection output...
- for ( var t in this.datatypes ) {
- var cat_outputs = new Array();
- cat_outputs = cat_outputs.concat(other.datatypes);
- if (other.node.post_job_actions){
- for (var pja_i in other.node.post_job_actions){
- var pja = other.node.post_job_actions[pja_i];
- if (pja.action_type == "ChangeDatatypeAction" && (pja.output_name == '' || pja.output_name == other.name) && pja.action_arguments){
- cat_outputs.push(pja.action_arguments['newtype']);
- }
- }
- }
- // FIXME: No idea what to do about case when datatype is 'input'
- for ( var other_datatype_i in cat_outputs ) {
- var other_datatype = cat_outputs[other_datatype_i];
- if ( other_datatype == "input" || other_datatype == "input_collection" || issubtype( cat_outputs[other_datatype_i], this.datatypes[t] ) ) {
- return true;
- }
- }
- }
- return false;
- },
- _otherCollectionType: function( other ) {
- var otherCollectionType = NULL_COLLECTION_TYPE_DESCRIPTION;
- if( other.isDataCollectionInput ) {
- otherCollectionType = other.collectionType;
- } else {
- var otherMapOver = other.mapOver();
- if( otherMapOver.isCollection ) {
- otherCollectionType = otherMapOver;
- }
- }
- return otherCollectionType;
- },
- } );
- var InputTerminal = BaseInputTerminal.extend( {
- update: function( input ) {
- this.datatypes = input.extensions;
- this.multiple = input.multiple;
- this.collection = false;
- },
- connect: function( connector ) {
- BaseInputTerminal.prototype.connect.call( this, connector );
- var other_output = connector.handle1;
- if( ! other_output ) {
- return;
- }
- var otherCollectionType = this._otherCollectionType( other_output );
- if( otherCollectionType.isCollection ) {
- this.setMapOver( otherCollectionType );
- }
- },
- attachable: function( other ) {
- var otherCollectionType = this._otherCollectionType( other );
- var thisMapOver = this.mapOver();
- if( otherCollectionType.isCollection ) {
- if( this.multiple ) {
- if( this.connected() ) {
- return false;
- }
- if( otherCollectionType.rank == 1 ) {
- return this._producesAcceptableDatatype( other );
- } else {
- // TODO: Allow subcollection mapping over this as if it were
- // a list collection input.
- return false;
- }
- }
- if( thisMapOver.isCollection && thisMapOver.canMatch( otherCollectionType ) ) {
- return this._producesAcceptableDatatype( other );
- } else {
- // Need to check if this would break constraints...
- var mappingConstraints = this._mappingConstraints();
- if( mappingConstraints.every( _.bind( otherCollectionType.canMatch, otherCollectionType ) ) ) {
- return this._producesAcceptableDatatype( other );
- } else {
- return false;
- }
- }
- } else if( thisMapOver.isCollection ) {
- // Attempting to match a non-collection output to an
- // explicitly collection input.
- return false;
- }
- return this._producesAcceptableDatatype( other );
- }
- });
- var InputCollectionTerminal = BaseInputTerminal.extend( {
- update: function( input ) {
- this.multiple = false;
- this.collection = true;
- this.datatypes = input.extensions;
- if( input.collection_type ) {
- this.collectionType = new CollectionTypeDescription( input.collection_type );
- } else {
- this.collectionType = ANY_COLLECTION_TYPE_DESCRIPTION;
- }
- },
- connect: function( connector ) {
- BaseInputTerminal.prototype.connect.call( this, connector );
- var other = connector.handle1;
- if( ! other ) {
- return;
- }
- var effectiveMapOver = this._effectiveMapOver( other );
- this.setMapOver( effectiveMapOver );
- },
- _effectiveMapOver: function( other ) {
- var collectionType = this.collectionType;
- var otherCollectionType = this._otherCollectionType( other );
- if( ! collectionType.canMatch( otherCollectionType ) ) {
- return otherCollectionType.effectiveMapOver( collectionType );
- } else {
- return NULL_COLLECTION_TYPE_DESCRIPTION;
- }
- },
- _effectiveCollectionType: function( ) {
- var collectionType = this.collectionType;
- var thisMapOver = this.mapOver();
- return thisMapOver.append( collectionType );
- },
- attachable: function ( other ) {
- var otherCollectionType = this._otherCollectionType( other );
- if( otherCollectionType.isCollection ) {
- var effectiveCollectionType = this._effectiveCollectionType( );
- var thisMapOver = this.mapOver();
- if( effectiveCollectionType.canMatch( otherCollectionType ) ) {
- // Only way a direct match...
- return this._producesAcceptableDatatype( other );
- // Otherwise we need to mapOver
- } else if( thisMapOver.isCollection ) {
- // In this case, mapOver already set and we didn't match skipping...
- return false;
- } else if( otherCollectionType.canMapOver( this.collectionType ) ) {
- var effectiveMapOver = this._effectiveMapOver( other );
- if( ! effectiveMapOver.isCollection ) {
- return false;
- }
- // Need to check if this would break constraints...
- var mappingConstraints = this._mappingConstraints();
- if( mappingConstraints.every( effectiveMapOver.canMatch ) ) {
- return this._producesAcceptableDatatype( other );
- }
- }
- }
- return false;
- }
- });
- var OutputCollectionTerminal = Terminal.extend( {
- initialize: function( attr ) {
- Terminal.prototype.initialize.call( this, attr );
- this.datatypes = attr.datatypes;
- this.collectionType = new CollectionTypeDescription( attr.collection_type );
- this.isDataCollectionInput = true;
- },
- update: function( output ) {
- var newCollectionType = new CollectionTypeDescription( output.collection_type );
- if( newCollectionType.collectionType != this.collectionType.collectionType ) {
- _.each( this.connectors, function( connector ) {
- // TODO: consider checking if connection valid before removing...
- connector.destroy();
- } );
- }
- this.collectionType = newCollectionType;
- }
- } );
- function Connector( handle1, handle2 ) {
- this.canvas = null;
- this.dragging = false;
- this.inner_color = "#FFFFFF";
- this.outer_color = "#D8B365";
- if ( handle1 && handle2 ) {
- this.connect( handle1, handle2 );
- }
- }
- $.extend( Connector.prototype, {
- connect: function ( t1, t2 ) {
- this.handle1 = t1;
- if ( this.handle1 ) {
- this.handle1.connect( this );
- }
- this.handle2 = t2;
- if ( this.handle2 ) {
- this.handle2.connect( this );
- }
- },
- destroy : function () {
- if ( this.handle1 ) {
- this.handle1.disconnect( this );
- }
- if ( this.handle2 ) {
- this.handle2.disconnect( this );
- }
- $(this.canvas).remove();
- },
- destroyIfInvalid: function() {
- if( this.handle1 && this.handle2 && ! this.handle2.attachable( this.handle1 ) ) {
- this.destroy();
- }
- },
- redraw : function () {
- var canvas_container = $("#canvas-container");
- if ( ! this.canvas ) {
- this.canvas = document.createElement( "canvas" );
- // excanvas specific hack
- if ( window.G_vmlCanvasManager ) {
- G_vmlCanvasManager.initElement( this.canvas );
- }
- canvas_container.append( $(this.canvas) );
- if ( this.dragging ) {
- this.canvas.style.zIndex = "300";
- }
- }
- var relativeLeft = function( e ) {
- return $(e).offset().left - canvas_container.offset().left;
- };
- var relativeTop = function( e ) {
- return $(e).offset().top - canvas_container.offset().top;
- };
- if (!this.handle1 || !this.handle2) {
- return;
- }
- // Find the position of each handle
- var start_x = relativeLeft( this.handle1.element ) + 5;
- var start_y = relativeTop( this.handle1.element ) + 5;
- var end_x = relativeLeft( this.handle2.element ) + 5;
- var end_y = relativeTop( this.handle2.element ) + 5;
- // Calculate canvas area
- var canvas_extra = 100;
- var canvas_min_x = Math.min( start_x, end_x );
- var canvas_max_x = Math.max( start_x, end_x );
- var canvas_min_y = Math.min( start_y, end_y );
- var canvas_max_y = Math.max( start_y, end_y );
- var cp_shift = Math.min( Math.max( Math.abs( canvas_max_y - canvas_min_y ) / 2, 100 ), 300 );
- var canvas_left = canvas_min_x - canvas_extra;
- var canvas_top = canvas_min_y - canvas_extra;
- var canvas_width = canvas_max_x - canvas_min_x + 2 * canvas_extra;
- var canvas_height = canvas_max_y - canvas_min_y + 2 * canvas_extra;
- // Place the canvas
- this.canvas.style.left = canvas_left + "px";
- this.canvas.style.top = canvas_top + "px";
- this.canvas.setAttribute( "width", canvas_width );
- this.canvas.setAttribute( "height", canvas_height );
- // Adjust points to be relative to the canvas
- start_x -= canvas_left;
- start_y -= canvas_top;
- end_x -= canvas_left;
- end_y -= canvas_top;
- // Draw the line
- var c = this.canvas.getContext("2d"),
- start_offsets = null,
- end_offsets = null;
- var num_offsets = 1;
- if ( this.handle1 && this.handle1.isMappedOver() ) {
- var start_offsets = [ -6, -3, 0, 3, 6 ];
- num_offsets = 5;
- } else {
- var start_offsets = [ 0 ];
- }
- if ( this.handle2 && this.handle2.isMappedOver() ) {
- var end_offsets = [ -6, -3, 0, 3, 6 ];
- num_offsets = 5;
- } else {
- var end_offsets = [ 0 ];
- }
- var connector = this;
- for( var i = 0; i < num_offsets; i++ ) {
- var inner_width = 5,
- outer_width = 7;
- if( start_offsets.length > 1 || end_offsets.length > 1 ) {
- // We have a multi-run, using many lines, make them small.
- inner_width = 1;
- outer_width = 3;
- }
- connector.draw_outlined_curve( start_x, start_y, end_x, end_y, cp_shift, inner_width, outer_width, start_offsets[ i % start_offsets.length ], end_offsets[ i % end_offsets.length ] );
- }
- },
- draw_outlined_curve : function( start_x, start_y, end_x, end_y, cp_shift, inner_width, outer_width, offset_start, offset_end ) {
- var offset_start = offset_start || 0;
- var offset_end = offset_end || 0;
- var c = this.canvas.getContext("2d");
- c.lineCap = "round";
- c.strokeStyle = this.outer_color;
- c.lineWidth = outer_width;
- c.beginPath();
- c.moveTo( start_x, start_y + offset_start );
- c.bezierCurveTo( start_x + cp_shift, start_y + offset_start, end_x - cp_shift, end_y + offset_end, end_x, end_y + offset_end);
- c.stroke();
- // Inner line
- c.strokeStyle = this.inner_color;
- c.lineWidth = inner_width;
- c.beginPath();
- c.moveTo( start_x, start_y + offset_start );
- c.bezierCurveTo( start_x + cp_shift, start_y + offset_start, end_x - cp_shift, end_y + offset_end, end_x, end_y + offset_end );
- c.stroke();
- }
- } );
- var Node = Backbone.Model.extend({
- initialize: function( attr ) {
- this.element = attr.element;
- this.input_terminals = {};
- this.output_terminals = {};
- this.tool_errors = {};
- },
- connectedOutputTerminals: function() {
- return this._connectedTerminals( this.output_terminals );
- },
- _connectedTerminals: function( terminals ) {
- var connectedTerminals = [];
- $.each( terminals, function( _, t ) {
- if( t.connectors.length > 0 ) {
- connectedTerminals.push( t );
- }
- } );
- return connectedTerminals;
- },
- hasConnectedOutputTerminals: function() {
- // return this.connectedOutputTerminals().length > 0; <- optimized this
- var outputTerminals = this.output_terminals;
- for( var outputName in outputTerminals ) {
- if( outputTerminals[ outputName ].connectors.length > 0 ) {
- return true;
- }
- }
- return false;
- },
- connectedMappedInputTerminals: function() {
- return this._connectedMappedTerminals( this.input_terminals );
- },
- hasConnectedMappedInputTerminals: function() {
- // return this.connectedMappedInputTerminals().length > 0; <- optimized this
- var inputTerminals = this.input_terminals;
- for( var inputName in inputTerminals ) {
- var inputTerminal = inputTerminals[ inputName ];
- if( inputTerminal.connectors.length > 0 && inputTerminal.isMappedOver() ) {
- return true;
- }
- }
- return false;
- },
- _connectedMappedTerminals: function( terminals ) {
- var mapped_outputs = [];
- $.each( terminals, function( _, t ) {
- var mapOver = t.mapOver();
- if( mapOver.isCollection ) {
- if( t.connectors.length > 0 ) {
- mapped_outputs.push( t );
- }
- }
- });
- return mapped_outputs;
- },
- mappedInputTerminals: function() {
- return this._mappedTerminals( this.input_terminals );
- },
- _mappedTerminals: function( terminals ) {
- var mappedTerminals = [];
- $.each( terminals, function( _, t ) {
- var mapOver = t.mapOver();
- if( mapOver.isCollection ) {
- mappedTerminals.push( t );
- }
- } );
- return mappedTerminals;
- },
- hasMappedOverInputTerminals: function() {
- var found = false;
- _.each( this.input_terminals, function( t ) {
- var mapOver = t.mapOver();
- if( mapOver.isCollection ) {
- found = true;
- }
- } );
- return found;
- },
- redraw : function () {
- $.each( this.input_terminals, function( _, t ) {
- t.redraw();
- });
- $.each( this.output_terminals, function( _, t ) {
- t.redraw();
- });
- },
- destroy : function () {
- $.each( this.input_terminals, function( k, t ) {
- t.destroy();
- });
- $.each( this.output_terminals, function( k, t ) {
- t.destroy();
- });
- workflow.remove_node( this );
- $(this.element).remove();
- },
- make_active : function () {
- $(this.element).addClass( "toolForm-active" );
- },
- make_inactive : function () {
- // Keep inactive nodes stacked from most to least recently active
- // by moving element to the end of parent's node list
- var element = this.element.get(0);
- (function(p) { p.removeChild( element ); p.appendChild( element ); })(element.parentNode);
- // Remove active class
- $(element).removeClass( "toolForm-active" );
- },
- init_field_data : function ( data ) {
- if ( data.type ) {
- this.type = data.type;
- }
- this.name = data.name;
- this.form_html = data.form_html;
- this.tool_state = data.tool_state;
- this.tool_errors = data.tool_errors;
- this.tooltip = data.tooltip ? data.tooltip : "";
- this.annotation = data.annotation;
- this.post_job_actions = data.post_job_actions ? data.post_job_actions : {};
- this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : [];
- var node = this;
- var nodeView = new NodeView({
- el: this.element[ 0 ],
- node: node,
- });
- node.nodeView = nodeView;
- $.each( data.data_inputs, function( i, input ) {
- nodeView.addDataInput( input );
- });
- if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) {
- nodeView.addRule();
- }
- $.each( data.data_outputs, function( i, output ) {
- nodeView.addDataOutput( output );
- } );
- nodeView.render();
- workflow.node_changed( this );
- },
- update_field_data : function( data ) {
- var node = this;
- nodeView = node.nodeView;
- this.tool_state = data.tool_state;
- this.form_html = data.form_html;
- this.tool_errors = data.tool_errors;
- this.annotation = data['annotation'];
- if( "post_job_actions" in data ) {
- // Won't be present in response for data inputs
- var pja_in = $.parseJSON(data.post_job_actions);
- this.post_job_actions = pja_in ? pja_in : {};
- }
- node.nodeView.renderToolErrors();
- // Update input rows
- var old_body = nodeView.$( "div.inputs" );
- var new_body = nodeView.newInputsDiv();
- var newTerminalViews = {};
- _.each( data.data_inputs, function( input ) {
- var terminalView = node.nodeView.addDataInput( input, new_body );
- newTerminalViews[ input.name ] = terminalView;
- });
- // Cleanup any leftover terminals
- _.each( _.difference( _.values( nodeView.terminalViews ), _.values( newTerminalViews ) ), function( unusedView ) {
- unusedView.el.terminal.destroy();
- } );
- nodeView.terminalViews = newTerminalViews;
- // In general workflow editor assumes tool outputs don't change in # or
- // type (not really valid right?) but adding special logic here for
- // data collection input parameters that can have their collection
- // change.
- if( data.data_outputs.length == 1 && "collection_type" in data.data_outputs[ 0 ] ) {
- nodeView.updateDataOutput( data.data_outputs[ 0 ] );
- }
- old_body.replaceWith( new_body );
- // If active, reactivate with new form_html
- this.markChanged();
- this.redraw();
- },
- error : function ( text ) {
- var b = $(this.element).find( ".toolFormBody" );
- b.find( "div" ).remove();
- var tmp = "<div style='color: red; text-style: italic;'>" + text + "</div>";
- this.form_html = tmp;
- b.html( tmp );
- workflow.node_changed( this );
- },
- markChanged: function() {
- workflow.node_changed( this );
- }
- } );
- function Workflow( canvas_container ) {
- this.canvas_container = canvas_container;
- this.id_counter = 0;
- this.nodes = {};
- this.name = null;
- this.has_changes = false;
- this.active_form_has_changes = false;
- }
- $.extend( Workflow.prototype, {
- add_node : function( node ) {
- node.id = this.id_counter;
- node.element.attr( 'id', 'wf-node-step-' + node.id );
- this.id_counter++;
- this.nodes[ node.id ] = node;
- this.has_changes = true;
- node.workflow = this;
- },
- remove_node : function( node ) {
- if ( this.active_node == node ) {
- this.clear_active_node();
- }
- delete this.nodes[ node.id ] ;
- this.has_changes = true;
- },
- remove_all : function() {
- wf = this;
- $.each( this.nodes, function ( k, v ) {
- v.destroy();
- wf.remove_node( v );
- });
- },
- rectify_workflow_outputs : function() {
- // Find out if we're using workflow_outputs or not.
- var using_workflow_outputs = false;
- var has_existing_pjas = false;
- $.each( this.nodes, function ( k, node ) {
- if (node.workflow_outputs && node.workflow_outputs.length > 0){
- using_workflow_outputs = true;
- }
- $.each(node.post_job_actions, function(pja_id, pja){
- if (pja.action_type === "HideDatasetAction"){
- has_existing_pjas = true;
- }
- });
- });
- if (using_workflow_outputs !== false || has_existing_pjas !== false){
- // Using workflow outputs, or has existing pjas. Remove all PJAs and recreate based on outputs.
- $.each(this.nodes, function (k, node ){
- if (node.type === 'tool'){
- var node_changed = false;
- if (node.post_job_actions == null){
- node.post_job_actions = {};
- node_changed = true;
- }
- var pjas_to_rem = [];
- $.each(node.post_job_actions, function(pja_id, pja){
- if (pja.action_type == "HideDatasetAction"){
- pjas_to_rem.push(pja_id);
- }
- });
- if (pjas_to_rem.length > 0 ) {
- $.each(pjas_to_rem, function(i, pja_name){
- node_changed = true;
- delete node.post_job_actions[pja_name];
- });
- }
- if (using_workflow_outputs){
- $.each(node.output_terminals, function(ot_id, ot){
- var create_pja = true;
- $.each(node.workflow_outputs, function(i, wo_name){
- if (ot.name === wo_name){
- create_pja = false;
- }
- });
- if (create_pja === true){
- node_changed = true;
- var pja = {
- action_type : "HideDatasetAction",
- output_name : ot.name,
- action_arguments : {}
- }
- node.post_job_actions['HideDatasetAction'+ot.name] = null;
- node.post_job_actions['HideDatasetAction'+ot.name] = pja;
- }
- });
- }
- // lastly, if this is the active node, and we made changes, reload the display at right.
- if (workflow.active_node == node && node_changed === true) {
- workflow.reload_active_node();
- }
- }
- });
- }
- },
- to_simple : function () {
- var nodes = {};
- $.each( this.nodes, function ( i, node ) {
- var input_connections = {};
- $.each( node.input_terminals, function ( k, t ) {
- input_connections[ t.name ] = null;
- // There should only be 0 or 1 connectors, so this is
- // really a sneaky if statement
- var cons = []
- $.each( t.connectors, function ( i, c ) {
- cons[i] = { id: c.handle1.node.id, output_name: c.handle1.name };
- input_connections[ t.name ] = cons;
- });
- });
- var post_job_actions = {};
- if (node.post_job_actions){
- $.each( node.post_job_actions, function ( i, act ) {
- var pja = {
- action_type : act.action_type,
- output_name : act.output_name,
- action_arguments : act.action_arguments
- }
- post_job_actions[ act.action_type + act.output_name ] = null;
- post_job_actions[ act.action_type + act.output_name ] = pja;
- });
- }
- if (!node.workflow_outputs){
- node.workflow_outputs = [];
- // Just in case.
- }
- var node_data = {
- id : node.id,
- type : node.type,
- tool_id : node.tool_id,
- tool_state : node.tool_state,
- tool_errors : node.tool_errors,
- input_connections : input_connections,
- position : $(node.element).position(),
- annotation: node.annotation,
- post_job_actions: node.post_job_actions,
- workflow_outputs: node.workflow_outputs
- };
- nodes[ node.id ] = node_data;
- });
- return { steps: nodes };
- },
- from_simple : function ( data ) {
- wf = this;
- var max_id = 0;
- wf.name = data.name;
- // First pass, nodes
- var using_workflow_outputs = false;
- $.each( data.steps, function( id, step ) {
- var node = prebuild_node( step.type, step.name, step.tool_id );
- node.init_field_data( step );
- if ( step.position ) {
- node.element.css( { top: step.position.top, left: step.position.left } );
- }
- node.id = step.id;
- wf.nodes[ node.id ] = node;
- max_id = Math.max( max_id, parseInt( id ) );
- // For older workflows, it's possible to have HideDataset PJAs, but not WorkflowOutputs.
- // Check for either, and then add outputs in the next pass.
- if (!using_workflow_outputs && node.type === 'tool'){
- if (node.workflow_outputs.length > 0){
- using_workflow_outputs = true;
- }
- else{
- $.each(node.post_job_actions, function(pja_id, pja){
- if (pja.action_type === "HideDatasetAction"){
- using_workflow_outputs = true;
- }
- });
- }
- }
- });
- wf.id_counter = max_id + 1;
- // Second pass, connections
- $.each( data.steps, function( id, step ) {
- var node = wf.nodes[id];
- $.each( step.input_connections, function( k, v ) {
- if ( v ) {
- if ( ! $.isArray( v ) ) {
- v = [ v ];
- }
- $.each( v, function( l, x ) {
- var other_node = wf.nodes[ x.id ];
- var c = new Connector();
- c.connect( other_node.output_terminals[ x.output_name ],
- node.input_terminals[ k ] );
- c.redraw();
- });
- }
- });
- if(using_workflow_outputs && node.type === 'tool'){
- // Ensure that every output terminal has a WorkflowOutput or HideDatasetAction.
- $.each(node.output_terminals, function(ot_id, ot){
- if(node.post_job_actions['HideDatasetAction'+ot.name] === undefined){
- node.workflow_outputs.push(ot.name);
- callout = $(node.element).find('.callout.'+ot.name);
- callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
- workflow.has_changes = true;
- }
- });
- }
- });
- },
- check_changes_in_active_form : function() {
- // If active form has changed, save it
- if (this.active_form_has_changes) {
- this.has_changes = true;
- // Submit form.
- $("#right-content").find("form").submit();
- this.active_form_has_changes = false;
- }
- },
- reload_active_node : function() {
- if (this.active_node){
- var node = this.active_node;
- this.clear_active_node();
- this.activate_node(node);
- }
- },
- clear_active_node : function() {
- if ( this.active_node ) {
- this.active_node.make_inactive();
- this.active_node = null;
- }
- parent.show_form_for_tool( "<div>No node selected</div>" );
- },
- activate_node : function( node ) {
- if ( this.active_node != node ) {
- this.check_changes_in_active_form();
- this.clear_active_node();
- parent.show_form_for_tool( node.form_html + node.tooltip, node );
- node.make_active();
- this.active_node = node;
- }
- },
- node_changed : function ( node ) {
- this.has_changes = true;
- if ( this.active_node == node ) {
- // Reactive with new form_html
- this.check_changes_in_active_form(); //Force changes to be saved even on new connection (previously dumped)
- parent.show_form_for_tool( node.form_html + node.tooltip, node );
- }
- },
- layout : function () {
- this.check_changes_in_active_form();
- this.has_changes = true;
- // Prepare predecessor / successor tracking
- var n_pred = {};
- var successors = {};
- // First pass to initialize arrays even for nodes with no connections
- $.each( this.nodes, function( id, node ) {
- if ( n_pred[id] === undefined ) { n_pred[id] = 0; }
- if ( successors[id] === undefined ) { successors[id] = []; }
- });
- // Second pass to count predecessors and successors
- $.each( this.nodes, function( id, node ) {
- $.each( node.input_terminals, function ( j, t ) {
- $.each( t.connectors, function ( k, c ) {
- // A connection exists from `other` to `node`
- var other = c.handle1.node;
- // node gains a predecessor
- n_pred[node.id] += 1;
- // other gains a successor
- successors[other.id].push( node.id );
- });
- });
- });
- // Assemble order, tracking levels
- node_ids_by_level = [];
- while ( true ) {
- // Everything without a predecessor
- level_parents = [];
- for ( var pred_k in n_pred ) {
- if ( n_pred[ pred_k ] == 0 ) {
- level_parents.push( pred_k );
- }
- }
- if ( level_parents.length == 0 ) {
- break;
- }
- node_ids_by_level.push( level_parents );
- // Remove the parents from this level, and decrement the number
- // of predecessors for each successor
- for ( var k in level_parents ) {
- var v = level_parents[k];
- delete n_pred[v];
- for ( var sk in successors[v] ) {
- n_pred[ successors[v][sk] ] -= 1;
- }
- }
- }
- if ( n_pred.length ) {
- // ERROR: CYCLE! Currently we do nothing
- return;
- }
- // Layout each level
- var all_nodes = this.nodes;
- var h_pad = 80; v_pad = 30;
- var left = h_pad;
- $.each( node_ids_by_level, function( i, ids ) {
- // We keep nodes in the same order in a level to give the user
- // some control over ordering
- ids.sort( function( a, b ) {
- return $(all_nodes[a].element).position().top - $(all_nodes[b].element).position().top;
- });
- // Position each node
- var max_width = 0;
- var top = v_pad;
- $.each( ids, function( j, id ) {
- var node = all_nodes[id];
- var element = $(node.element);
- $(element).css( { top: top, left: left } );
- max_width = Math.max( max_width, $(element).width() );
- top += $(element).height() + v_pad;
- });
- left += max_width + h_pad;
- });
- // Need to redraw all connectors
- $.each( all_nodes, function( _, node ) { node.redraw(); } );
- },
- bounds_for_all_nodes: function() {
- var xmin = Infinity, xmax = -Infinity,
- ymin = Infinity, ymax = -Infinity,
- p;
- $.each( this.nodes, function( id, node ) {
- e = $(node.element);
- p = e.position();
- xmin = Math.min( xmin, p.left );
- xmax = Math.max( xmax, p.left + e.width() );
- ymin = Math.min( ymin, p.top );
- ymax = Math.max( ymax, p.top + e.width() );
- });
- return { xmin: xmin, xmax: xmax, ymin: ymin, ymax: ymax };
- },
- fit_canvas_to_nodes: function() {
- // Span of all elements
- var bounds = this.bounds_for_all_nodes();
- var position = this.canvas_container.position();
- var parent = this.canvas_container.parent();
- // Determine amount we need to expand on top/left
- var xmin_delta = fix_delta( bounds.xmin, 100 );
- var ymin_delta = fix_delta( bounds.ymin, 100 );
- // May need to expand farther to fill viewport
- xmin_delta = Math.max( xmin_delta, position.left );
- ymin_delta = Math.max( ymin_delta, position.top );
- var left = position.left - xmin_delta;
- var top = position.top - ymin_delta;
- // Same for width/height
- var width = round_up( bounds.xmax + 100, 100 ) + xmin_delta;
- var height = round_up( bounds.ymax + 100, 100 ) + ymin_delta;
- width = Math.max( width, - left + parent.width() );
- height = Math.max( height, - top + parent.height() );
- // Grow the canvas container
- this.canvas_container.css( {
- left: left,
- top: top,
- width: width,
- height: height
- });
- // Move elements back if needed
- this.canvas_container.children().each( function() {
- var p = $(this).position();
- $(this).css( "left", p.left + xmin_delta );
- $(this).css( "top", p.top + ymin_delta );
- });
- }
- });
- function fix_delta( x, n ) {
- if ( x < n|| x > 3*n ) {
- new_pos = ( Math.ceil( ( ( x % n ) ) / n ) + 1 ) * n;
- return ( - ( x - new_pos ) );
- }
- return 0;
- }
-
- function round_up( x, n ) {
- return Math.ceil( x / n ) * n;
- }
-
- function prebuild_node( type, title_text, tool_id ) {
- var f = $("<div class='toolForm toolFormInCanvas'></div>");
- var node = new Node( { element: f } );
- node.type = type;
- if ( type == 'tool' ) {
- node.tool_id = tool_id;
- }
- var title = $("<div class='toolFormTitle unselectable'>" + title_text + "</div>" );
- f.append( title );
- f.css( "left", $(window).scrollLeft() + 20 ); f.css( "top", $(window).scrollTop() + 20 );
- var b = $("<div class='toolFormBody'></div>");
- var tmp = "<div><img height='16' align='middle' src='" + galaxy_config.root + "static/images/loading_small_white_bg.gif'/> loading tool info...</div>";
- b.append( tmp );
- node.form_html = tmp;
- f.append( b );
- // Fix width to computed width
- // Now add floats
- var buttons = $("<div class='buttons' style='float: right;'></div>");
- buttons.append( $("<div>").addClass("fa-icon-button fa fa-times").click( function( e ) {
- node.destroy();
- }));
- // Place inside container
- f.appendTo( "#canvas-container" );
- // Position in container
- var o = $("#canvas-container").position();
- var p = $("#canvas-container").parent();
- var width = f.width();
- var height = f.height();
- f.css( { left: ( - o.left ) + ( p.width() / 2 ) - ( width / 2 ), top: ( - o.top ) + ( p.height() / 2 ) - ( height / 2 ) } );
- buttons.prependTo( title );
- width += ( buttons.width() + 10 );
- f.css( "width", width );
- $(f).bind( "dragstart", function() {
- workflow.activate_node( node );
- }).bind( "dragend", function() {
- workflow.node_changed( this );
- workflow.fit_canvas_to_nodes();
- canvas_manager.draw_overview();
- }).bind( "dragclickonly", function() {
- workflow.activate_node( node );
- }).bind( "drag", function( e, d ) {
- // Move
- var po = $(this).offsetParent().offset(),
- x = d.offsetX - po.left,
- y = d.offsetY - po.top;
- $(this).css( { left: x, top: y } );
- // Redraw
- $(this).find( ".terminal" ).each( function() {
- this.terminal.redraw();
- });
- });
- return node;
- }
- function add_node( type, title_text, tool_id ) {
- // Abstraction for use by galaxy.workflow.js to hide
- // some editor details from workflow code and reduce duplication
- // between add_node_for_tool and add_node_for_module.
- var node = prebuild_node( type, title_text, tool_id );
- workflow.add_node( node );
- workflow.fit_canvas_to_nodes();
- canvas_manager.draw_overview();
- workflow.activate_node( node );
- return node;
- }
- var ext_to_type = null;
- var type_to_type = null;
- function issubtype( child, parent ) {
- child = ext_to_type[child];
- parent = ext_to_type[parent];
- return ( type_to_type[child] ) && ( parent in type_to_type[child] );
- }
- function populate_datatype_info( data ) {
- ext_to_type = data.ext_to_class_name;
- type_to_type = data.class_to_classes;
- }
- //////////////
- // START VIEWS
- //////////////
- var NodeView = Backbone.View.extend( {
- initialize: function( options ){
- this.node = options.node;
- this.output_width = Math.max(150, this.$el.width());
- this.tool_body = this.$el.find( ".toolFormBody" );
- this.tool_body.find( "div" ).remove();
- this.newInputsDiv().appendTo( this.tool_body );
- this.terminalViews = {};
- this.outputTerminlViews = {};
- },
- render : function() {
- this.renderToolErrors();
- this.$el.css( "width", Math.min(250, Math.max(this.$el.width(), this.output_width )));
- },
- renderToolErrors: function( ) {
- if ( this.node.tool_errors ) {
- this.$el.addClass( "tool-node-error" );
- } else {
- this.$el.removeClass( "tool-node-error" );
- }
- },
- newInputsDiv: function() {
- return $("<div class='inputs'></div>");
- },
- updateMaxWidth: function( newWidth ) {
- this.output_width = Math.max( this.output_width, newWidth );
- },
- addRule: function() {
- this.tool_body.append( $( "<div class='rule'></div>" ) );
- },
- addDataInput: function( input, body ) {
- var skipResize = true;
- if( ! body ) {
- body = this.$( ".inputs" );
- // initial addition to node - resize input to help calculate node
- // width.
- skipResize = false;
- }
- var terminalView = this.terminalViews[ input.name ];
- var terminalViewClass = ( input.input_type == "dataset_collection" ) ? InputCollectionTerminalView : InputTerminalView;
- if( terminalView && ! ( terminalView instanceof terminalViewClass ) ) {
- terminalView.el.terminal.destroy();
- terminalView = null;
- }
- if( ! terminalView ) {
- terminalView = new terminalViewClass( {
- node: this.node,
- input: input
- } );
- } else {
- var terminal = terminalView.el.terminal;
- terminal.update( input );
- terminal.destroyInvalidConnections();
- }
- this.terminalViews[ input.name ] = terminalView;
- var terminalElement = terminalView.el;
- var inputView = new DataInputView( {
- terminalElement: terminalElement,
- input: input,
- nodeView: this,
- skipResize: skipResize
- } );
- var ib = inputView.$el;
- // Append to new body
- body.append( ib.prepend( terminalView.terminalElements() ) );
- return terminalView;
- },
- addDataOutput: function( output ) {
- var terminalViewClass = ( output.collection_type ) ? OutputCollectionTerminalView : OutputTerminalView;
- var terminalView = new terminalViewClass( {
- node: this.node,
- output: output
- } );
- var outputView = new DataOutputView( {
- "output": output,
- "terminalElement": terminalView.el,
- "nodeView": this,
- } );
- this.tool_body.append( outputView.$el.append( terminalView.terminalElements() ) );
- },
- updateDataOutput: function( output ) {
- var outputTerminal = this.node.output_terminals[ output.name ];
- outputTerminal.update( output );
- }
- } );
- var DataInputView = Backbone.View.extend( {
- className: "form-row dataRow input-data-row",
- initialize: function( options ){
- this.input = options.input;
- this.nodeView = options.nodeView;
- this.terminalElement = options.terminalElement;
- this.$el.attr( "name", this.input.name )
- .html( this.input.label );
- if( ! options.skipResize ) {
- this.$el.css({ position:'absolute',
- left: -1000,
- top: -1000,
- display:'none'});
- $('body').append(this.el);
- this.nodeView.updateMaxWidth( this.$el.outerWidth() );
- this.$el.css({ position:'',
- left:'',
- top:'',
- display:'' });
- this.$el.remove();
- }
- },
- } );
- var OutputCalloutView = Backbone.View.extend( {
- tagName: "div",
- initialize: function( options ) {
- this.label = options.label;
- this.node = options.node;
- this.output = options.output;
- var view = this;
- this.$el
- .attr( "class", 'callout '+this.label )
- .css( { display: 'none' } )
- .append(
- $("<div class='buttons'></div>").append(
- $("<img/>").attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png').click( function() {
- if ($.inArray(view.output.name, view.node.workflow_outputs) != -1){
- view.node.workflow_outputs.splice($.inArray(view.output.name, view.node.workflow_outputs), 1);
- view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png');
- }else{
- view.node.workflow_outputs.push(view.output.name);
- view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
- }
- workflow.has_changes = true;
- canvas_manager.draw_overview();
- })))
- .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." });
-
- this.$el.css({
- top: '50%',
- margin:'-8px 0px 0px 0px',
- right: 8
- });
- this.$el.show();
- this.resetImage();
- },
- resetImage: function() {
- if ($.inArray( this.output.name, this.node.workflow_outputs) === -1){
- this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png');
- } else{
- this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
- }
- },
- hoverImage: function() {
- this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-yellow.png');
- }
- } );
- var DataOutputView = Backbone.View.extend( {
- className: "form-row dataRow",
- initialize: function( options ) {
- this.output = options.output;
- this.terminalElement = options.terminalElement;
- this.nodeView = options.nodeView;
- var output = this.output;
- var label = output.name;
- var node = this.nodeView.node;
- var isInput = output.extensions.indexOf( 'input' ) >= 0 || output.extensions.indexOf( 'input_collection' ) >= 0;
- if ( ! isInput ) {
- label = label + " (" + output.extensions.join(", ") + ")";
- }
- this.$el.html( label )
- if (node.type == 'tool'){
- var calloutView = new OutputCalloutView( {
- "label": label,
- "output": output,
- "node": node,
- });
- this.$el.append( calloutView.el );
- this.$el.hover( function() { calloutView.hoverImage() }, function() { calloutView.resetImage() } );
- }
- this.$el.css({ position:'absolute',
- left: -1000,
- top: -1000,
- display:'none'});
- $('body').append( this.el );
- this.nodeView.updateMaxWidth( this.$el.outerWidth() + 17 );
- this.$el.css({ position:'',
- left:'',
- top:'',
- display:'' })
- .detach();
- }
- } );
- var TerminalView = Backbone.View.extend( {
- setupMappingView: function( terminal ) {
- var terminalMapping = new this.terminalMappingClass( { terminal: terminal } );
- var terminalMappingView = new this.terminalMappingViewClass( { model: terminalMapping } );
- terminalMappingView.render();
- terminal.terminalMappingView = terminalMappingView;
- this.terminalMappingView = terminalMappingView;
- },
- terminalElements: function() {
- if( this.terminalMappingView ) {
- return [ this.terminalMappingView.el, this.el ];
- } else{
- return [ this.el ];
- }
- }
- } );
- var BaseInputTerminalView = TerminalView.extend( {
- className: "terminal input-terminal",
- initialize: function( options ) {
- var node = options.node;
- var input = options.input;
- var name = input.name;
- var terminal = this.terminalForInput( input );
- if( ! terminal.multiple ) {
- this.setupMappingView( terminal );
- }
- this.el.terminal = terminal;
- terminal.node = node;
- terminal.name = name;
- node.input_terminals[name] = terminal;
- },
- events: {
- "dropinit": "onDropInit",
- "dropstart": "onDropStart",
- "dropend": "onDropEnd",
- "drop": "onDrop",
- "hover": "onHover",
- },
- onDropInit: function( e, d ) {
- var terminal = this.el.terminal;
- // Accept a dragable if it is an output terminal and has a
- // compatible type
- return $(d.drag).hasClass( "output-terminal" ) && terminal.canAccept( d.drag.terminal );
- },
- onDropStart: function( e, d ) {
- if (d.proxy.terminal) {
- d.proxy.terminal.connectors[0].inner_color = "#BBFFBB";
- }
- },
- onDropEnd: function ( e, d ) {
- if (d.proxy.terminal) {
- d.proxy.terminal.connectors[0].inner_color = "#FFFFFF";
- }
- },
- onDrop: function( e, d ) {
- var terminal = this.el.terminal;
- new Connector( d.drag.terminal, terminal ).redraw();
- },
- onHover: function() {
- var element = this.el;
- var terminal = element.terminal;
- // If connected, create a popup to allow disconnection
- if ( terminal.connectors.length > 0 ) {
- // Create callout
- var t = $("<div class='callout'></div>")
- .css( { display: 'none' } )
- .appendTo( "body" )
- .append(
- $("<div class='button'></div>").append(
- $("<div/>").addClass("fa-icon-button fa fa-times").click( function() {
- $.each( terminal.connectors, function( _, x ) {
- if (x) {
- x.destroy();
- }
- });
- t.remove();
- })))
- .bind( "mouseleave", function() {
- $(this).remove();
- });
- // Position it and show
- t.css({
- top: $(element).offset().top - 2,
- left: $(element).offset().left - t.width(),
- 'padding-right': $(element).width()
- }).show();
- }
- },
- } );
- var InputTerminalView = BaseInputTerminalView.extend( {
- terminalMappingClass: InputTerminalMapping,
- terminalMappingViewClass: InputTerminalMappingView,
- terminalForInput: function( input ) {
- return new InputTerminal( { element: this.el, input: input } );
- },
- } );
- var InputCollectionTerminalView = BaseInputTerminalView.extend( {
- terminalMappingClass: InputCollectionTerminalMapping,
- terminalMappingViewClass: InputCollectionTerminalMappingView,
- terminalForInput: function( input ) {
- return new InputCollectionTerminal( { element: this.el, input: input } );
- },
- } );
- var BaseOutputTerminalView = TerminalView.extend( {
- className: "terminal output-terminal",
- initialize: function( options ) {
- var node = options.node;
- var output = options.output;
- var name = output.name;
- var terminal = this.terminalForOutput( output );
- this.setupMappingView( terminal );
- this.el.terminal = terminal;
- terminal.node = node;
- terminal.name = name;
- node.output_terminals[name] = terminal;
- },
- events: {
- "drag": "onDrag",
- "dragstart": "onDragStart",
- "dragend": "onDragEnd",
- },
- onDrag: function ( e, d ) {
- var onmove = function() {
- var po = $(d.proxy).offsetParent().offset(),
- x = d.offsetX - po.left,
- y = d.offsetY - po.top;
- $(d.proxy).css( { left: x, top: y } );
- d.proxy.terminal.redraw();
- // FIXME: global
- canvas_manager.update_viewport_overlay();
- };
- onmove();
- $("#canvas-container").get(0).scroll_panel.test( e, onmove );
- },
- onDragStart: function( e, d ) {
- $( d.available ).addClass( "input-terminal-active" );
- // Save PJAs in the case of change datatype actions.
- workflow.check_changes_in_active_form();
- // Drag proxy div
- var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' )
- .appendTo( "#canvas-container" ).get(0);
- // Terminal and connection to display noodle while dragging
- h.terminal = new OutputTerminal( { element: h } );
- var c = new Connector();
- c.dragging = true;
- c.connect( this.el.terminal, h.terminal );
- return h;
- },
- onDragEnd: function ( e, d ) {
- d.proxy.terminal.connectors[0].destroy();
- $(d.proxy).remove();
- $( d.available ).removeClass( "input-terminal-active" );
- $("#canvas-container").get(0).scroll_panel.stop();
- }
- } );
- var OutputTerminalView = BaseOutputTerminalView.extend( {
- terminalMappingClass: OutputTerminalMapping,
- terminalMappingViewClass: OutputTerminalMappingView,
- terminalForOutput: function( output ) {
- var type = output.extensions;
- var terminal = new OutputTerminal( { element: this.el, datatypes: type } );
- return terminal;
- },
- } );
- var OutputCollectionTerminalView = BaseOutputTerminalView.extend( {
- terminalMappingClass: OutputCollectionTerminalMapping,
- terminalMappingViewClass: OutputCollectionTerminalMappingView,
- terminalForOutput: function( output ) {
- var collection_type = output.collection_type;
- var terminal = new OutputCollectionTerminal( { element: this.el, collection_type: collection_type, datatypes: output.extensions } );
- return terminal;
- },
- } );
- ////////////
- // END VIEWS
- ////////////
- // FIXME: merge scroll panel into CanvasManager, clean up hardcoded stuff.
- function ScrollPanel( panel ) {
- this.panel = panel;
- }
- $.extend( ScrollPanel.prototype, {
- test: function( e, onmove ) {
- clearTimeout( this.timeout );
- var x = e.pageX,
- y = e.pageY,
- // Panel size and position
- panel = $(this.panel),
- panel_pos = panel.position(),
- panel_w = panel.width(),
- panel_h = panel.height(),
- // Viewport size and offset
- viewport = panel.parent(),
- viewport_w = viewport.width(),
- viewport_h = viewport.height(),
- viewport_offset = viewport.offset(),
- // Edges of viewport (in page coordinates)
- min_x = viewport_offset.left,
- min_y = viewport_offset.top,
- max_x = min_x + viewport.width(),
- max_y = min_y + viewport.height(),
- // Legal panel range
- p_min_x = - ( panel_w - ( viewport_w / 2 ) ),
- p_min_y = - ( panel_h - ( viewport_h / 2 )),
- p_max_x = ( viewport_w / 2 ),
- p_max_y = ( viewport_h / 2 ),
- // Did the panel move?
- moved = false,
- // Constants
- close_dist = 5,
- nudge = 23;
- if ( x - close_dist < min_x ) {
- if ( panel_pos.left < p_max_x ) {
- var t = Math.min( nudge, p_max_x - panel_pos.left );
- panel.css( "left", panel_pos.left + t );
- moved = true;
- }
- } else if ( x + close_dist > max_x ) {
- if ( panel_pos.left > p_min_x ) {
- var t = Math.min( nudge, panel_pos.left - p_min_x );
- panel.css( "left", panel_pos.left - t );
- moved = true;
- }
- } else if ( y - close_dist < min_y ) {
- if ( panel_pos.top < p_max_y ) {
- var t = Math.min( nudge, p_max_y - panel_pos.top );
- panel.css( "top", panel_pos.top + t );
- moved = true;
- }
- } else if ( y + close_dist > max_y ) {
- if ( panel_pos.top > p_min_y ) {
- var t = Math.min( nudge, panel_pos.top - p_min_x );
- panel.css( "top", ( panel_pos.top - t ) + "px" );
- moved = true;
- }
- }
- if ( moved ) {
- // Keep moving even if mouse doesn't move
- onmove();
- var panel = this;
- this.timeout = setTimeout( function() { panel.test( e, onmove ); }, 50 );
- }
- },
- stop: function( e, ui ) {
- clearTimeout( this.timeout );
- }
- });
- function CanvasManager( canvas_viewport, overview ) {
- this.cv = canvas_viewport;
- this.cc = this.cv.find( "#canvas-container" );
- this.oc = overview.find( "#overview-canvas" );
- this.ov = overview.find( "#overview-viewport" );
- // Make overview box draggable
- this.init_drag();
- }
- $.extend( CanvasManager.prototype, {
- init_drag : function () {
- var self = this;
- var move = function( x, y ) {
- x = Math.min( x, self.cv.width() / 2 );
- x = Math.max( x, - self.cc.width() + self.cv.width() / 2 );
- y = Math.min( y, self.cv.height() / 2 );
- y = Math.max( y, - self.cc.height() + self.cv.height() / 2 );
- self.cc.css( {
- left: x,
- top: y
- });
- self.update_viewport_overlay();
- };
- // Dragging within canvas background
- this.cc.each( function() {
- this.scroll_panel = new ScrollPanel( this );
- });
- var x_adjust, y_adjust;
- this.cv.bind( "dragstart", function() {
- var o = $(this).offset();
- var p = self.cc.position();
- y_adjust = p.top - o.top;
- x_adjust = p.left - o.left;
- }).bind( "drag", function( e, d ) {
- move( d.offsetX + x_adjust, d.offsetY + y_adjust );
- }).bind( "dragend", function() {
- workflow.fit_canvas_to_nodes();
- self.draw_overview();
- });
- // Dragging for overview pane
- this.ov.bind( "drag", function( e, d ) {
- var in_w = self.cc.width(),
- in_h = self.cc.height(),
- o_w = self.oc.width(),
- o_h = self.oc.height(),
- p = $(this).offsetParent().offset(),
- new_x_offset = d.offsetX - p.left,
- new_y_offset = d.offsetY - p.top;
- move( - ( new_x_offset / o_w * in_w ),
- - ( new_y_offset / o_h * in_h ) );
- }).bind( "dragend", function() {
- workflow.fit_canvas_to_nodes();
- self.draw_overview();
- });
- // Dragging for overview border (resize)
- $("#overview-border").bind( "drag", function( e, d ) {
- var op = $(this).offsetParent();
- var opo = op.offset();
- var new_size = Math.max( op.width() - ( d.offsetX - opo.left ),
- op.height() - ( d.offsetY - opo.top ) );
- $(this).css( {
- width: new_size,
- height: new_size
- });
- self.draw_overview();
- });
-
- /* Disable dragging for child element of the panel so that resizing can
- only be done by dragging the borders */
- $("#overview-border div").bind("drag", function() { });
-
- },
- update_viewport_overlay: function() {
- var cc = this.cc,
- cv = this.cv,
- oc = this.oc,
- ov = this.ov,
- in_w = cc.width(),
- in_h = cc.height(),
- o_w = oc.width(),
- o_h = oc.height(),
- cc_pos = cc.position();
- ov.css( {
- left: - ( cc_pos.left / in_w * o_w ),
- top: - ( cc_pos.top / in_h * o_h ),
- // Subtract 2 to account for borders (maybe just change box sizing style instead?)
- width: ( cv.width() / in_w * o_w ) - 2,
- height: ( cv.height() / in_h * o_h ) - 2
- });
- },
- draw_overview: function() {
- var canvas_el = $("#overview-canvas"),
- size = canvas_el.parent().parent().width(),
- c = canvas_el.get(0).getContext("2d"),
- in_w = $("#canvas-container").width(),
- in_h = $("#canvas-container").height();
- var o_h, shift_h, o_w, shift_w;
- // Fit canvas into overview area
- var cv_w = this.cv.width();
- var cv_h = this.cv.height();
- if ( in_w < cv_w && in_h < cv_h ) {
- // Canvas is smaller than viewport
- o_w = in_w / cv_w * size;
- shift_w = ( size - o_w ) / 2;
- o_h = in_h / cv_h * size;
- shift_h = ( size - o_h ) / 2;
- } else if ( in_w < in_h ) {
- // Taller than wide
- shift_h = 0;
- o_h = size;
- o_w = Math.ceil( o_h * in_w / in_h );
- shift_w = ( size - o_w ) / 2;
- } else {
- // Wider than tall
- o_w = size;
- shift_w = 0;
- o_h = Math.ceil( o_w * in_h / in_w );
- shift_h = ( size - o_h ) / 2;
- }
- canvas_el.parent().css( {
- left: shift_w,
- top: shift_h,
- width: o_w,
- height: o_h
- });
- canvas_el.attr( "width", o_w );
- canvas_el.attr( "height", o_h );
- // Draw overview
- $.each( workflow.nodes, function( id, node ) {
- c.fillStyle = "#D2C099";
- c.strokeStyle = "#D8B365";
- c.lineWidth = 1;
- var node_element = $(node.element),
- position = node_element.position(),
- x = position.left / in_w * o_w,
- y = position.top / in_h * o_h,
- w = node_element.width() / in_w * o_w,
- h = node_element.height() / in_h * o_h;
- if (node.tool_errors){
- c.fillStyle = "#FFCCCC";
- c.strokeStyle = "#AA6666";
- } else if (node.workflow_outputs != undefined && node.workflow_outputs.length > 0){
- c.fillStyle = "#E8A92D";
- c.strokeStyle = "#E8A92D";
- }
- c.fillRect( x, y, w, h );
- c.strokeRect( x, y, w, h );
- });
- this.update_viewport_overlay();
- }
- });