mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Detail bug report from Michael Crusoe here : https://trello.com/c/0mdGCx4P. This also fixes a test case added in 289e48b which was both attempting to assert something wrong and was incorrectly implemented. Augmenting the workflow editor test suite with some actually valid test cases that assert the correct behaviors. For a longer explaination - workflow input terminals have two related concepts 'canAccept' and 'attachable'. An output terminal is 'attachable' if in the abstract it could be attached to the input regardless of whether the input is already filled or not. 'canAccept' is more stateful in that an output terminal is 'canAccept'able if the input terminal is not filled ('_inputFilled') and it is 'attachable'. So - the problem was 'attachable' was not correctly defined for a multiple input data parameters. 'attachable' was asserting that any connected input terminal could not be 'attachable' by an collection output terminal - so on these asynchronous node state changes collections attached to multiple input data parameters were being wiped out. The more percise/correct distinction is that if a multiple input data parameter has single inputs connected to it - it cannot also have a collection connected to it (yet anyway). This fixes the mentioned bug. The problematic test case was conflating 'attachable' and 'canAccept'able - I have fixed the test case to verify the correct 'attachable' logic and added newer, higher level test cases to test the canAccept logic and the actual behavior the end user would observe (of the connector being destroy).
1992 lines
72 KiB
JavaScript
1992 lines
72 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._collectionAttached()) {
|
|
// Can only attach one collection to multiple input
|
|
// data parameter.
|
|
inputsFilled = true;
|
|
} else {
|
|
inputFilled = false;
|
|
}
|
|
} else {
|
|
inputFilled = true;
|
|
}
|
|
}
|
|
return inputFilled;
|
|
},
|
|
_collectionAttached: function( ) {
|
|
if( ! this.connected() ) {
|
|
return false;
|
|
} else {
|
|
var firstOutput = this.connectors[ 0 ].handle1;
|
|
if( ! firstOutput ){
|
|
return false;
|
|
} else {
|
|
if( firstOutput.isDataCollectionInput || firstOutput.isMappedOver() || firstOutput.datatypes.indexOf( "input_collection" ) > 0 ) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_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() && ! this._collectionAttached() ) {
|
|
// if single inputs attached, cannot also attach a
|
|
// collection (yet...)
|
|
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 ) {
|
|
var connector = d.proxy.terminal.connectors[0];
|
|
// check_changes_in_active_form may change the state and cause a
|
|
// the connection to have already been destroyed. There must be better
|
|
// ways to handle this but the following check fixes some serious GUI
|
|
// bugs for now.
|
|
if(connector) {
|
|
connector.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();
|
|
}
|
|
});
|