Files
galaxy/static/scripts/galaxy.workflow_editor.canvas.js
T
2014-05-27 12:52:41 -05:00

1974 lines
71 KiB
JavaScript

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 === null ){
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();
}
});