function Terminal( element ) { this.element = element; this.connectors = []; } $.extend( Terminal.prototype, { connect: function ( connector ) { this.connectors.push( connector ); if ( this.node ) { this.node.changed(); } }, disconnect: function ( connector ) { this.connectors.splice( $.inArray( connector, this.connectors ), 1 ); if ( this.node ) { this.node.changed(); } }, redraw: function () { $.each( this.connectors, function( _, c ) { c.redraw(); }); }, destroy: function () { $.each( this.connectors.slice(), function( _, c ) { c.destroy(); }); } }); function OutputTerminal( element, datatypes ) { Terminal.call( this, element ); this.datatypes = datatypes; } OutputTerminal.prototype = new Terminal(); function InputTerminal( element, datatypes ) { Terminal.call( this, element ); this.datatypes = datatypes; } InputTerminal.prototype = new Terminal(); $.extend( InputTerminal.prototype, { can_accept: function ( other ) { if ( this.connectors.length < 1 ) { 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 ) { if ( cat_outputs[other_datatype_i] == "input" || issubtype( cat_outputs[other_datatype_i], this.datatypes[t] ) ) { return true; } } } } return false; } }); 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; this.handle1.connect( this ); this.handle2 = t2; this.handle2.connect( this ); }, destroy : function () { if ( this.handle1 ) { this.handle1.disconnect( this ); } if ( this.handle2 ) { this.handle2.disconnect( this ); } $(this.canvas).remove(); }, 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; }; // 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"); c.lineCap = "round"; c.strokeStyle = this.outer_color; c.lineWidth = 7; c.beginPath(); c.moveTo( start_x, start_y ); c.bezierCurveTo( start_x + cp_shift, start_y, end_x - cp_shift, end_y, end_x, end_y ); c.stroke(); // Inner line c.strokeStyle = this.inner_color; c.lineWidth = 5; c.beginPath(); c.moveTo( start_x, start_y ); c.bezierCurveTo( start_x + cp_shift, start_y, end_x - cp_shift, end_y, end_x, end_y ); c.stroke(); } } ); function Node( element ) { this.element = element; this.input_terminals = {}; this.output_terminals = {}; this.tool_errors = {}; } $.extend( Node.prototype, { enable_input_terminal : function( elements, name, types ) { var node = this; $(elements).each( function() { var terminal = this.terminal = new InputTerminal( this, types ); terminal.node = node; terminal.name = name; $(this).bind( "dropstart", function( e ) { e.dragProxy.terminal.connectors[0].inner_color = "#BBFFBB"; }).bind( "dropend", function ( e ) { e.dragProxy.terminal.connectors[0].inner_color = "#FFFFFF"; }).bind( "drop", function( e ) { ( new Connector( e.dragTarget.terminal, e.dropTarget.terminal ) ).redraw(); }).bind( "hover", function() { // If connected, create a popup to allow disconnection if ( terminal.connectors.length > 0 ) { // Create callout var t = $("
") .css( { display: 'none' } ) .appendTo( "body" ) .append( $("
").append( $("").attr("src", image_path + '/delete_icon.png').click( function() { $.each( terminal.connectors, function( _, x ) { x.destroy(); }); t.remove(); }))) .bind( "mouseleave", function() { $(this).remove(); }); // Position it and show t.css({ top: $(this).offset().top - 2, left: $(this).offset().left - t.width(), 'padding-right': $(this).width() }).show(); } }); node.input_terminals[name] = terminal; }); }, enable_output_terminal : function( elements, name, type ) { var node = this; $(elements).each( function() { var terminal_element = this; var terminal = this.terminal = new OutputTerminal( this, type ); terminal.node = node; terminal.name = name; $(this).bind( "dragstart", function( e ) { workflow.check_changes_in_active_form(); //To save PJAs in the case of change datatype actions. var h = $( '
' ).appendTo( "#canvas-container" ).get(0); h.terminal = new OutputTerminal( h ); var c = new Connector(); c.dragging = true; c.connect( this.terminal, h.terminal ); $.dropManage({ filter: function( e ) { return this.terminal.can_accept( terminal ); } }).addClass( "input-terminal-active" ); return h; }).bind( "drag", function ( e ) { var onmove = function() { var po = $(e.dragProxy).offsetParent().offset(), x = e.offsetX - po.left, y = e.offsetY - po.top; $(e.dragProxy).css( { left: x, top: y } ); e.dragProxy.terminal.redraw(); // FIXME: global canvas_manager.update_viewport_overlay(); }; onmove(); $("#canvas-container").get(0).scroll_panel.test( e, onmove ); }).bind( "dragend", function ( e ) { e.dragProxy.terminal.connectors[0].destroy(); $(e.dragProxy).remove(); $.dropManage().removeClass( "input-terminal-active" ); $("#canvas-container").get(0).scroll_panel.stop(); }); node.output_terminals[name] = terminal; }); }, 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 ) { var f = this.element; 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; if (data.workflow_outputs){ this.workflow_outputs = data.workflow_outputs; }else{ this.workflow_outputs = []; } if ( this.tool_errors ) { f.addClass( "tool-node-error" ); } else { f.removeClass( "tool-node-error" ); } var node = this; var output_width = Math.max(150, f.width()); var b = f.find( ".toolFormBody" ); b.find( "div" ).remove(); var ibox = $("
").appendTo( b ); $.each( data.data_inputs, function( i, input ) { var t = $("
"); node.enable_input_terminal( t, input.name, input.extensions ); var ib = $("
" + input.label + "
" ); ib.css({ position:'absolute', left: -1000, top: -1000, display:'none'}); $('body').append(ib); output_width = Math.max(output_width, ib.outerWidth()); ib.css({ position:'', left:'', top:'', display:'' }); $('body').remove(ib); ibox.append( ib.prepend( t ) ); }); if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) { b.append( $( "
" ) ); } $.each( data.data_outputs, function( i, output ) { var t = $( "
" ); node.enable_output_terminal( t, output.name, output.extensions ); var label = output.name; if ( output.extensions.indexOf( 'input' ) < 0 ) { label = label + " (" + output.extensions.join(", ") + ")"; } var r = $("
" + label + "
" ); if (node.type == 'tool'){ var callout = $("
") .css( { display: 'none' } ) .append( $("
").append( $("").attr('src', image_path + '/fugue/asterisk-small-outline.png').click( function() { if ($.inArray(output.name, node.workflow_outputs) != -1){ node.workflow_outputs.splice($.inArray(output.name, node.workflow_outputs), 1); callout.find('img').attr('src', image_path + '/fugue/asterisk-small-outline.png'); }else{ node.workflow_outputs.push(output.name); callout.find('img').attr('src', image_path + '/fugue/asterisk-small.png'); } workflow.has_changes = true; canvas_manager.draw_overview(); }))) .tipsy({delayIn:500, fallback: "Flag this as a workflow output. All non-flagged outputs will be hidden." }); callout.css({ top: '50%', margin:'-8px 0px 0px 0px', right: 8 }); callout.show(); r.append(callout); if ($.inArray(output.name, node.workflow_outputs) == -1){ callout.find('img').attr('src', image_path + '/fugue/asterisk-small-outline.png'); }else{ callout.find('img').attr('src', image_path + '/fugue/asterisk-small.png'); } r.bind( "hover", function() { callout.find('img').attr('src', image_path + '/fugue/asterisk-small-yellow.png'); }); r.bind( "mouseleave", function() { callout.find('img').attr('src', image_path + '/fugue/asterisk-small.png'); if ($.inArray(output.name, node.workflow_outputs) == -1){ callout.find('img').attr('src', image_path + '/fugue/asterisk-small-outline.png'); }else{ callout.find('img').attr('src', image_path + '/fugue/asterisk-small.png'); } }); } r.css({ position:'absolute', left: -1000, top: -1000, display:'none'}); $('body').append(r); output_width = Math.max(output_width, r.outerWidth() + 17); r.css({ position:'', left:'', top:'', display:'' }); $('body').remove(r); b.append( r.append( t ) ); }); f.css( "width", Math.min(250, Math.max(f.width(), output_width ))); workflow.node_changed( this ); }, update_field_data : function( data ) { var el = $(this.element), node = this; this.tool_state = data.tool_state; this.form_html = data.form_html; this.tool_errors = data.tool_errors; this.annotation = data['annotation']; this.post_job_actions = $.parseJSON(data.post_job_actions); if ( this.tool_errors ) { el.addClass( "tool-node-error" ); } else { el.removeClass( "tool-node-error" ); } // Update input rows var old_body = el.find( "div.inputs" ); var new_body = $("
"); var old = old_body.find( "div.input-data-row"); $.each( data.data_inputs, function( i, input ) { var t = $("
"); node.enable_input_terminal( t, input.name, input.extensions ); // If already connected save old connection old_body.find( "div[name=" + input.name + "]" ).each( function() { $(this).find( ".input-terminal" ).each( function() { var c = this.terminal.connectors[0]; if ( c ) { t[0].terminal.connectors[0] = c; c.handle2 = t[0].terminal; } }); $(this).remove(); }); // Append to new body new_body.append( $("
" + input.label + "
" ).prepend( t ) ); }); old_body.replaceWith( new_body ); // Cleanup any leftover terminals old_body.find( "div.input-data-row > .terminal" ).each( function() { this.terminal.destroy(); }); // If active, reactivate with new form_html this.changed(); this.redraw(); }, error : function ( text ) { var b = $(this.element).find( ".toolFormBody" ); b.find( "div" ).remove(); var tmp = "
" + text + "
"; this.form_html = tmp; b.html( tmp ); workflow.node_changed( this ); }, changed: 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 ); }); }, 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 $.each( t.connectors, function ( i, c ) { input_connections[ t.name ] = { id: c.handle1.node.id, output_name: c.handle1.name }; }); }); var post_job_actions = {}; if (node.post_job_actions){ $.each( node.post_job_actions, function ( i, act ) { var pja = { job_id : act.id, 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 $.each( data.steps, function( id, step ) { var node = prebuild_node( "tool", 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 ) ); }); 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 ) { var other_node = wf.nodes[ v.id ]; var c = new Connector(); c.connect( other_node.output_terminals[ v.output_name ], node.input_terminals[ k ] ); c.redraw(); } }); }); }, 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; } }, clear_active_node : function() { if ( this.active_node ) { this.active_node.make_inactive(); this.active_node = null; } parent.show_form_for_tool( "
No node selected
" ); }, 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 = $("
"); var node = new Node( f ); node.type = type; if ( type == 'tool' ) { node.tool_id = tool_id; } var title = $("
" + title_text + "
" ); f.append( title ); f.css( "left", $(window).scrollLeft() + 20 ); f.css( "top", $(window).scrollTop() + 20 ); var b = $("
"); var tmp = "
loading tool info...
"; b.append( tmp ); node.form_html = tmp; f.append( b ); // Fix width to computed width // Now add floats var buttons = $("
"); buttons.append( $("").attr("src", image_path + '/delete_icon.png').click( function( e ) { node.destroy(); } ).hover( function() { $(this).attr( "src", image_path + "/delete_icon_dark.png" ); }, function() { $(this).attr( "src", image_path + "/delete_icon.png" ); } ) ); // 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 ) { // Move var po = $(this).offsetParent().offset(), x = e.offsetX - po.left, y = e.offsetY - po.top; $(this).css( { left: x, top: y } ); // Redraw $(this).find( ".terminal" ).each( function() { this.terminal.redraw(); }); }); 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; } // 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( e ) { 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 ) { move( e.offsetX + x_adjust, e.offsetY + y_adjust ); }).bind( "dragend", function() { workflow.fit_canvas_to_nodes(); self.draw_overview(); }); // Dragging for overview pane this.ov.bind( "drag", function( e ) { 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 = e.offsetX - p.left, new_y_offset = e.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 ) { var op = $(this).offsetParent(); var opo = op.offset(); var new_size = Math.max( op.width() - ( e.offsetX - opo.left ), op.height() - ( e.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(e) { }); }, 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.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(); } });