mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
1974 lines
71 KiB
JavaScript
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();
|
|
}
|
|
});
|