PageRenderTime 100ms CodeModel.GetById 24ms app.highlight 65ms RepoModel.GetById 1ms app.codeStats 1ms

/static/scripts/galaxy.workflow_editor.canvas.js

https://bitbucket.org/cistrome/cistrome-harvard/
JavaScript | 1132 lines | 1024 code | 21 blank | 87 comment | 124 complexity | 0530ba11f1e33b6f980d635ff1d408d8 MD5 | raw file
   1function Terminal( element ) {
   2    this.element = element;
   3    this.connectors = [];
   4}
   5$.extend( Terminal.prototype, {
   6    connect: function ( connector ) {
   7        this.connectors.push( connector );
   8        if ( this.node ) {
   9            this.node.changed();
  10        }
  11    },
  12    disconnect: function ( connector ) {
  13        this.connectors.splice( $.inArray( connector, this.connectors ), 1 );
  14        if ( this.node ) {
  15            this.node.changed();
  16        }
  17    },
  18    redraw: function () {
  19        $.each( this.connectors, function( _, c ) {
  20            c.redraw();  
  21        });
  22    },
  23    destroy: function () {
  24        $.each( this.connectors.slice(), function( _, c ) {
  25            c.destroy();
  26        });
  27    }
  28});
  29
  30function OutputTerminal( element, datatypes ) {
  31    Terminal.call( this, element );
  32    this.datatypes = datatypes;
  33}
  34
  35OutputTerminal.prototype = new Terminal();
  36
  37function InputTerminal( element, datatypes, multiple ) {
  38    Terminal.call( this, element );
  39    this.datatypes = datatypes;
  40    this.multiple = multiple
  41}
  42
  43InputTerminal.prototype = new Terminal();
  44
  45$.extend( InputTerminal.prototype, {
  46    can_accept: function ( other ) {
  47        if ( this.connectors.length < 1 || this.multiple) {
  48            for ( var t in this.datatypes ) {
  49                var cat_outputs = new Array();
  50                cat_outputs = cat_outputs.concat(other.datatypes);
  51                if (other.node.post_job_actions){
  52                    for (var pja_i in other.node.post_job_actions){
  53                        var pja = other.node.post_job_actions[pja_i];
  54                        if (pja.action_type == "ChangeDatatypeAction" && (pja.output_name == '' || pja.output_name == other.name) && pja.action_arguments){
  55                            cat_outputs.push(pja.action_arguments['newtype']);
  56                        }
  57                    }
  58                }
  59                // FIXME: No idea what to do about case when datatype is 'input'
  60                for ( var other_datatype_i in cat_outputs ) {
  61                    if ( cat_outputs[other_datatype_i] == "input" || issubtype( cat_outputs[other_datatype_i], this.datatypes[t] ) ) {
  62                        return true;
  63                    }
  64                }
  65            }
  66        }
  67        return false;
  68    }
  69});
  70
  71function Connector( handle1, handle2 ) {
  72    this.canvas = null;
  73    this.dragging = false;
  74    this.inner_color = "#FFFFFF";
  75    this.outer_color = "#D8B365";
  76    if ( handle1 && handle2 ) {
  77        this.connect( handle1, handle2 );
  78    }
  79}
  80$.extend( Connector.prototype, {
  81    connect: function ( t1, t2 ) {
  82        this.handle1 = t1;
  83        if ( this.handle1 ) {
  84            this.handle1.connect( this );
  85        }
  86        this.handle2 = t2;
  87        if ( this.handle2 ) {
  88            this.handle2.connect( this );
  89        }
  90    },
  91    destroy : function () {
  92        if ( this.handle1 ) {
  93            this.handle1.disconnect( this );
  94        }
  95        if ( this.handle2 ) {
  96            this.handle2.disconnect( this );
  97        }
  98        $(this.canvas).remove();
  99    },
 100    redraw : function () {
 101        var canvas_container = $("#canvas-container");
 102        if ( ! this.canvas ) {
 103            this.canvas = document.createElement( "canvas" );
 104            // excanvas specific hack
 105            if ( window.G_vmlCanvasManager ) {
 106                G_vmlCanvasManager.initElement( this.canvas );
 107            }
 108            canvas_container.append( $(this.canvas) );
 109            if ( this.dragging ) {
 110                this.canvas.style.zIndex = "300";
 111            }
 112        }
 113        var relativeLeft = function( e ) {
 114            return $(e).offset().left - canvas_container.offset().left;
 115        };
 116        var relativeTop = function( e ) {
 117            return $(e).offset().top - canvas_container.offset().top;
 118        };
 119        if (!this.handle1 || !this.handle2) {
 120            return;
 121        }
 122        // Find the position of each handle
 123        var start_x = relativeLeft( this.handle1.element ) + 5;
 124        var start_y = relativeTop( this.handle1.element ) + 5;
 125        var end_x = relativeLeft( this.handle2.element ) + 5;
 126        var end_y = relativeTop( this.handle2.element ) + 5;
 127        // Calculate canvas area
 128        var canvas_extra = 100;
 129        var canvas_min_x = Math.min( start_x, end_x );
 130        var canvas_max_x = Math.max( start_x, end_x );
 131        var canvas_min_y = Math.min( start_y, end_y );
 132        var canvas_max_y = Math.max( start_y, end_y );
 133        var cp_shift = Math.min( Math.max( Math.abs( canvas_max_y - canvas_min_y ) / 2, 100 ), 300 );
 134        var canvas_left = canvas_min_x - canvas_extra;
 135        var canvas_top = canvas_min_y - canvas_extra;
 136        var canvas_width = canvas_max_x - canvas_min_x + 2 * canvas_extra;
 137        var canvas_height = canvas_max_y - canvas_min_y + 2 * canvas_extra;
 138        // Place the canvas
 139        this.canvas.style.left = canvas_left + "px";
 140        this.canvas.style.top = canvas_top + "px";
 141        this.canvas.setAttribute( "width", canvas_width );
 142        this.canvas.setAttribute( "height", canvas_height );
 143        // Adjust points to be relative to the canvas
 144        start_x -= canvas_left;
 145        start_y -= canvas_top;
 146        end_x -= canvas_left;
 147        end_y -= canvas_top;
 148        // Draw the line
 149        var c = this.canvas.getContext("2d");
 150        c.lineCap = "round";
 151        c.strokeStyle = this.outer_color;
 152        c.lineWidth = 7;
 153        c.beginPath();
 154        c.moveTo( start_x, start_y );
 155        c.bezierCurveTo( start_x + cp_shift, start_y, end_x - cp_shift, end_y, end_x, end_y );
 156        c.stroke();
 157        // Inner line
 158        c.strokeStyle = this.inner_color;
 159        c.lineWidth = 5;
 160        c.beginPath();
 161        c.moveTo( start_x, start_y );
 162        c.bezierCurveTo( start_x + cp_shift, start_y, end_x - cp_shift, end_y, end_x, end_y );
 163        c.stroke();
 164    }
 165} );
 166
 167function Node( element ) {
 168    this.element = element;
 169    this.input_terminals = {};
 170    this.output_terminals = {};
 171    this.tool_errors = {};
 172}
 173$.extend( Node.prototype, {
 174    new_input_terminal : function( input ) {
 175        var t = $("<div class='terminal input-terminal'></div>");
 176        this.enable_input_terminal( t, input.name, input.extensions, input.multiple );
 177        return t;
 178    },
 179    enable_input_terminal : function( elements, name, types, multiple ) {
 180        var node = this;
 181        $(elements).each( function() {
 182            var terminal = this.terminal = new InputTerminal( this, types, multiple );
 183            terminal.node = node;
 184            terminal.name = name;
 185            $(this).bind( "dropinit", function( e, d ) {
 186                // Accept a dragable if it is an output terminal and has a
 187                // compatible type
 188                return $(d.drag).hasClass( "output-terminal" ) && terminal.can_accept( d.drag.terminal );
 189            }).bind( "dropstart", function( e, d  ) {
 190                if (d.proxy.terminal) { 
 191                    d.proxy.terminal.connectors[0].inner_color = "#BBFFBB";
 192                }
 193            }).bind( "dropend", function ( e, d ) {
 194                if (d.proxy.terminal) { 
 195                    d.proxy.terminal.connectors[0].inner_color = "#FFFFFF";
 196                }
 197            }).bind( "drop", function( e, d ) {
 198                ( new Connector( d.drag.terminal, terminal ) ).redraw();
 199            }).bind( "hover", function() {
 200                // If connected, create a popup to allow disconnection
 201                if ( terminal.connectors.length > 0 ) {
 202                    // Create callout
 203                    var t = $("<div class='callout'></div>")
 204                        .css( { display: 'none' } )
 205                        .appendTo( "body" )
 206                        .append(
 207                            $("<div class='button'></div>").append(
 208                                $("<div/>").addClass("fa-icon-button fa fa-times").click( function() {
 209                                    $.each( terminal.connectors, function( _, x ) {
 210                                        if (x) {
 211                                            x.destroy();
 212                                        }
 213                                    });
 214                                    t.remove();
 215                                })))
 216                        .bind( "mouseleave", function() {
 217                            $(this).remove();
 218                        });
 219                    // Position it and show
 220                    t.css({
 221                            top: $(this).offset().top - 2,
 222                            left: $(this).offset().left - t.width(),
 223                            'padding-right': $(this).width()
 224                        }).show();
 225                }
 226            });
 227            node.input_terminals[name] = terminal;
 228        });
 229    },
 230    enable_output_terminal : function( elements, name, type ) {
 231        var node = this;
 232        $(elements).each( function() {
 233            var terminal_element = this;
 234            var terminal = this.terminal = new OutputTerminal( this, type );
 235            terminal.node = node;
 236            terminal.name = name;
 237            $(this).bind( "dragstart", function( e, d ) { 
 238                $( d.available ).addClass( "input-terminal-active" );
 239                // Save PJAs in the case of change datatype actions.
 240                workflow.check_changes_in_active_form(); 
 241                // Drag proxy div
 242                var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' )
 243                    .appendTo( "#canvas-container" ).get(0);
 244                // Terminal and connection to display noodle while dragging
 245                h.terminal = new OutputTerminal( h );
 246                var c = new Connector();
 247                c.dragging = true;
 248                c.connect( this.terminal, h.terminal );
 249                return h;
 250            }).bind( "drag", function ( e, d ) {
 251                var onmove = function() {
 252                    var po = $(d.proxy).offsetParent().offset(),
 253                        x = d.offsetX - po.left,
 254                        y = d.offsetY - po.top;
 255                    $(d.proxy).css( { left: x, top: y } );
 256                    d.proxy.terminal.redraw();
 257                    // FIXME: global
 258                    canvas_manager.update_viewport_overlay();
 259                };
 260                onmove();
 261                $("#canvas-container").get(0).scroll_panel.test( e, onmove );
 262            }).bind( "dragend", function ( e, d ) {
 263                d.proxy.terminal.connectors[0].destroy();
 264                $(d.proxy).remove();
 265                $( d.available ).removeClass( "input-terminal-active" );
 266                $("#canvas-container").get(0).scroll_panel.stop();
 267            });
 268            node.output_terminals[name] = terminal;
 269        });
 270    },
 271    redraw : function () {
 272        $.each( this.input_terminals, function( _, t ) {
 273            t.redraw();
 274        });
 275        $.each( this.output_terminals, function( _, t ) {
 276            t.redraw();
 277        });
 278    },
 279    destroy : function () {
 280        $.each( this.input_terminals, function( k, t ) {
 281            t.destroy();
 282        });
 283        $.each( this.output_terminals, function( k, t ) {
 284            t.destroy();
 285        });
 286        workflow.remove_node( this );
 287        $(this.element).remove();
 288    },
 289    make_active : function () {
 290        $(this.element).addClass( "toolForm-active" );
 291    },
 292    make_inactive : function () {
 293        // Keep inactive nodes stacked from most to least recently active
 294        // by moving element to the end of parent's node list
 295        var element = this.element.get(0);
 296        (function(p) { p.removeChild( element ); p.appendChild( element ); })(element.parentNode);
 297        // Remove active class
 298        $(element).removeClass( "toolForm-active" );
 299    },
 300    init_field_data : function ( data ) {
 301        var f = this.element;
 302        if ( data.type ) {
 303            this.type = data.type;
 304        }
 305        this.name = data.name;
 306        this.form_html = data.form_html;
 307        this.tool_state = data.tool_state;
 308        this.tool_errors = data.tool_errors;
 309        this.tooltip = data.tooltip ? data.tooltip : "";
 310        this.annotation = data.annotation;
 311        this.post_job_actions = data.post_job_actions ? data.post_job_actions : {};
 312        this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : [];
 313
 314        if ( this.tool_errors ) {
 315            f.addClass( "tool-node-error" );
 316        } else {
 317            f.removeClass( "tool-node-error" );
 318        }
 319        var node = this;
 320        var output_width = Math.max(150, f.width());
 321        var b = f.find( ".toolFormBody" );
 322        b.find( "div" ).remove();
 323        var ibox = $("<div class='inputs'></div>").appendTo( b );
 324        $.each( data.data_inputs, function( i, input ) {
 325            var t = node.new_input_terminal( input );
 326            var ib = $("<div class='form-row dataRow input-data-row' name='" + input.name + "'>" + input.label + "</div>" );
 327            ib.css({  position:'absolute',
 328                        left: -1000,
 329                        top: -1000,
 330                        display:'none'});
 331            $('body').append(ib);
 332            output_width = Math.max(output_width, ib.outerWidth());
 333            ib.css({ position:'',
 334                       left:'',
 335                       top:'',
 336                       display:'' });
 337            ib.remove();
 338            ibox.append( ib.prepend( t ) );
 339        });
 340        if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) {
 341            b.append( $( "<div class='rule'></div>" ) );
 342        }
 343        $.each( data.data_outputs, function( i, output ) {
 344            var t = $( "<div class='terminal output-terminal'></div>" );
 345            node.enable_output_terminal( t, output.name, output.extensions );
 346            var label = output.name;
 347            if ( output.extensions.indexOf( 'input' ) < 0 ) {
 348                label = label + " (" + output.extensions.join(", ") + ")";
 349            }
 350            var r = $("<div class='form-row dataRow'>" + label + "</div>" );
 351            if (node.type == 'tool'){
 352                var callout = $("<div class='callout "+label+"'></div>")
 353                    .css( { display: 'none' } )
 354                    .append(
 355                        $("<div class='buttons'></div>").append(
 356                            $("<img/>").attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png').click( function() {
 357                                if ($.inArray(output.name, node.workflow_outputs) != -1){
 358                                    node.workflow_outputs.splice($.inArray(output.name, node.workflow_outputs), 1);
 359                                    callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png');
 360                                }else{
 361                                    node.workflow_outputs.push(output.name);
 362                                    callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
 363                                }
 364                                workflow.has_changes = true;
 365                                canvas_manager.draw_overview();
 366                            })))
 367                    .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." });
 368                callout.css({
 369                        top: '50%',
 370                        margin:'-8px 0px 0px 0px',
 371                        right: 8
 372                    });
 373                callout.show();
 374                r.append(callout);
 375                if ($.inArray(output.name, node.workflow_outputs) === -1){
 376                    callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png');
 377                }else{
 378                    callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
 379                }
 380                r.hover(
 381                    function(){
 382                        callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-yellow.png');
 383                    },
 384                    function(){
 385                        if ($.inArray(output.name, node.workflow_outputs) === -1){
 386                            callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png');
 387                        }else{
 388                            callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
 389                        }
 390                    });
 391            }
 392            r.css({  position:'absolute',
 393                        left: -1000,
 394                        top: -1000,
 395                        display:'none'});
 396            $('body').append(r);
 397            output_width = Math.max(output_width, r.outerWidth() + 17);
 398            r.css({ position:'',
 399                       left:'',
 400                       top:'',
 401                       display:'' });
 402            r.detach();
 403            b.append( r.append( t ) );
 404        });
 405        f.css( "width", Math.min(250, Math.max(f.width(), output_width )));
 406        workflow.node_changed( this );
 407    },
 408    update_field_data : function( data ) {
 409        var el = $(this.element),
 410            node = this;
 411        this.tool_state = data.tool_state;
 412        this.form_html = data.form_html;
 413        this.tool_errors = data.tool_errors;
 414        this.annotation = data['annotation'];
 415        var pja_in = $.parseJSON(data.post_job_actions);
 416        this.post_job_actions = pja_in ? pja_in : {};
 417        if ( this.tool_errors ) {
 418                el.addClass( "tool-node-error" );
 419        } else {
 420                el.removeClass( "tool-node-error" );
 421        }
 422        // Update input rows
 423        var old_body = el.find( "div.inputs" );
 424        var new_body = $("<div class='inputs'></div>");
 425        var old = old_body.find( "div.input-data-row");
 426        $.each( data.data_inputs, function( i, input ) {
 427            var t = node.new_input_terminal( input );
 428            // If already connected save old connection
 429            old_body.find( "div[name='" + input.name + "']" ).each( function() {
 430                $(this).find( ".input-terminal" ).each( function() {
 431                    var c = this.terminal.connectors[0];
 432                    if ( c ) {
 433                        t[0].terminal.connectors[0] = c;
 434                        c.handle2 = t[0].terminal;
 435                    }
 436                });
 437                $(this).remove();
 438            });
 439            // Append to new body
 440            new_body.append( $("<div class='form-row dataRow input-data-row' name='" + input.name + "'>" + input.label + "</div>" ).prepend( t ) );
 441        });
 442        old_body.replaceWith( new_body );
 443        // Cleanup any leftover terminals
 444        old_body.find( "div.input-data-row > .terminal" ).each( function() {
 445            this.terminal.destroy();
 446        });
 447        // If active, reactivate with new form_html
 448        this.changed();
 449        this.redraw();
 450    },
 451    error : function ( text ) {
 452        var b = $(this.element).find( ".toolFormBody" );
 453        b.find( "div" ).remove();
 454        var tmp = "<div style='color: red; text-style: italic;'>" + text + "</div>";
 455        this.form_html = tmp;
 456        b.html( tmp );
 457        workflow.node_changed( this );
 458    },
 459    changed: function() {
 460        workflow.node_changed( this );
 461    }
 462} );
 463
 464function Workflow( canvas_container ) {
 465    this.canvas_container = canvas_container;
 466    this.id_counter = 0;
 467    this.nodes = {};
 468    this.name = null;
 469    this.has_changes = false;
 470    this.active_form_has_changes = false;
 471}
 472$.extend( Workflow.prototype, {
 473    add_node : function( node ) {
 474        node.id = this.id_counter;
 475        node.element.attr( 'id', 'wf-node-step-' + node.id );
 476        this.id_counter++;
 477        this.nodes[ node.id ] = node;
 478        this.has_changes = true;
 479        node.workflow = this;
 480    },
 481    remove_node : function( node ) {
 482        if ( this.active_node == node ) {
 483            this.clear_active_node();
 484        }
 485        delete this.nodes[ node.id ] ;
 486        this.has_changes = true;
 487    },
 488    remove_all : function() {
 489        wf = this;
 490        $.each( this.nodes, function ( k, v ) {
 491            v.destroy();
 492            wf.remove_node( v );
 493        });
 494    },
 495    rectify_workflow_outputs : function() {
 496        // Find out if we're using workflow_outputs or not.
 497        var using_workflow_outputs = false;
 498        var has_existing_pjas = false;
 499        $.each( this.nodes, function ( k, node ) {
 500            if (node.workflow_outputs && node.workflow_outputs.length > 0){
 501                using_workflow_outputs = true;
 502            }
 503            $.each(node.post_job_actions, function(pja_id, pja){
 504                if (pja.action_type === "HideDatasetAction"){
 505                    has_existing_pjas = true;
 506                }
 507            });
 508        });
 509        if (using_workflow_outputs !== false || has_existing_pjas !== false){
 510            // Using workflow outputs, or has existing pjas.  Remove all PJAs and recreate based on outputs.
 511            $.each(this.nodes, function (k, node ){
 512                if (node.type === 'tool'){
 513                    var node_changed = false;
 514                    if (node.post_job_actions == null){
 515                        node.post_job_actions = {};
 516                        node_changed = true;
 517                    }
 518                    var pjas_to_rem = [];
 519                    $.each(node.post_job_actions, function(pja_id, pja){
 520                        if (pja.action_type == "HideDatasetAction"){
 521                            pjas_to_rem.push(pja_id);
 522                        }
 523                    });
 524                    if (pjas_to_rem.length > 0 ) {
 525                        $.each(pjas_to_rem, function(i, pja_name){
 526                            node_changed = true;
 527                            delete node.post_job_actions[pja_name];
 528                        });
 529                    }
 530                    if (using_workflow_outputs){
 531                        $.each(node.output_terminals, function(ot_id, ot){
 532                            var create_pja = true;
 533                            $.each(node.workflow_outputs, function(i, wo_name){
 534                                if (ot.name === wo_name){
 535                                    create_pja = false;
 536                                }
 537                            });
 538                            if (create_pja === true){
 539                                node_changed = true;
 540                                var pja = {
 541                                    action_type : "HideDatasetAction",
 542                                    output_name : ot.name,
 543                                    action_arguments : {}
 544                                }
 545                                node.post_job_actions['HideDatasetAction'+ot.name] = null;
 546                                node.post_job_actions['HideDatasetAction'+ot.name] = pja;
 547                            }
 548                        });
 549                    }
 550                    // lastly, if this is the active node, and we made changes, reload the display at right.
 551                    if (workflow.active_node == node && node_changed === true) {
 552                        workflow.reload_active_node();
 553                    }
 554                }
 555            });
 556        }
 557    },
 558    to_simple : function () {
 559        var nodes = {};
 560        $.each( this.nodes, function ( i, node ) {
 561            var input_connections = {};
 562            $.each( node.input_terminals, function ( k, t ) {
 563                input_connections[ t.name ] = null;
 564                // There should only be 0 or 1 connectors, so this is
 565                // really a sneaky if statement
 566                var cons = []
 567                $.each( t.connectors, function ( i, c ) {
 568                    cons[i] = { id: c.handle1.node.id, output_name: c.handle1.name };
 569                    input_connections[ t.name ] = cons;
 570                });
 571            });
 572            var post_job_actions = {};
 573            if (node.post_job_actions){
 574                $.each( node.post_job_actions, function ( i, act ) {
 575                    var pja = {
 576                        action_type : act.action_type, 
 577                        output_name : act.output_name, 
 578                        action_arguments : act.action_arguments
 579                    }
 580                    post_job_actions[ act.action_type + act.output_name ] = null;
 581                    post_job_actions[ act.action_type + act.output_name ] = pja;
 582                });
 583            }
 584            if (!node.workflow_outputs){
 585                node.workflow_outputs = [];
 586                // Just in case.
 587            }
 588            var node_data = {
 589                id : node.id,
 590                type : node.type,
 591                tool_id : node.tool_id,
 592                tool_state : node.tool_state,
 593                tool_errors : node.tool_errors,
 594                input_connections : input_connections,
 595                position : $(node.element).position(),
 596                annotation: node.annotation,
 597                post_job_actions: node.post_job_actions,
 598                workflow_outputs: node.workflow_outputs
 599            };
 600            nodes[ node.id ] = node_data;
 601        });
 602        return { steps: nodes };
 603    },
 604    from_simple : function ( data ) {
 605        wf = this;
 606        var max_id = 0;
 607        wf.name = data.name;
 608        // First pass, nodes
 609        var using_workflow_outputs = false;
 610        $.each( data.steps, function( id, step ) {
 611            var node = prebuild_node( step.type, step.name, step.tool_id );
 612            node.init_field_data( step );
 613            if ( step.position ) {
 614                node.element.css( { top: step.position.top, left: step.position.left } );
 615            }
 616            node.id = step.id;
 617            wf.nodes[ node.id ] = node;
 618            max_id = Math.max( max_id, parseInt( id ) );
 619            // For older workflows, it's possible to have HideDataset PJAs, but not WorkflowOutputs.
 620            // Check for either, and then add outputs in the next pass.
 621            if (!using_workflow_outputs && node.type === 'tool'){
 622                if (node.workflow_outputs.length > 0){
 623                    using_workflow_outputs = true;
 624                }
 625                else{
 626                    $.each(node.post_job_actions, function(pja_id, pja){
 627                        if (pja.action_type === "HideDatasetAction"){
 628                            using_workflow_outputs = true;
 629                        }
 630                    });
 631                }
 632            }
 633        });
 634        wf.id_counter = max_id + 1;
 635        // Second pass, connections
 636        $.each( data.steps, function( id, step ) {
 637            var node = wf.nodes[id];
 638            $.each( step.input_connections, function( k, v ) {
 639                if ( v ) {
 640                    if ( ! $.isArray( v ) ) {
 641                        v = [ v ];
 642                    }
 643                    $.each( v, function( l, x ) {
 644                        var other_node = wf.nodes[ x.id ];
 645                        var c = new Connector();
 646                        c.connect( other_node.output_terminals[ x.output_name ],
 647                                   node.input_terminals[ k ] );
 648                        c.redraw();
 649                    });
 650                }
 651            });
 652            if(using_workflow_outputs && node.type === 'tool'){
 653                // Ensure that every output terminal has a WorkflowOutput or HideDatasetAction.
 654                $.each(node.output_terminals, function(ot_id, ot){
 655                    if(node.post_job_actions['HideDatasetAction'+ot.name] === undefined){
 656                        node.workflow_outputs.push(ot.name);
 657                        callout = $(node.element).find('.callout.'+ot.name);
 658                        callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
 659                        workflow.has_changes = true;
 660                    }
 661                });
 662            }
 663        });
 664    },
 665    check_changes_in_active_form : function() {
 666        // If active form has changed, save it
 667        if (this.active_form_has_changes) {
 668            this.has_changes = true;
 669            // Submit form.
 670            $("#right-content").find("form").submit();
 671            this.active_form_has_changes = false;
 672        }
 673    },
 674    reload_active_node : function() {
 675        if (this.active_node){
 676            var node = this.active_node;
 677            this.clear_active_node();
 678            this.activate_node(node);
 679        }
 680    },
 681    clear_active_node : function() {
 682        if ( this.active_node ) {
 683            this.active_node.make_inactive();
 684            this.active_node = null;
 685        }
 686        parent.show_form_for_tool( "<div>No node selected</div>" );
 687    },
 688    activate_node : function( node ) {
 689        if ( this.active_node != node ) {
 690            this.check_changes_in_active_form();
 691            this.clear_active_node();
 692            parent.show_form_for_tool( node.form_html + node.tooltip, node );
 693            node.make_active();
 694            this.active_node = node;
 695        }
 696    },
 697    node_changed : function ( node ) {
 698        this.has_changes = true;
 699        if ( this.active_node == node ) {
 700            // Reactive with new form_html
 701            this.check_changes_in_active_form(); //Force changes to be saved even on new connection (previously dumped)
 702            parent.show_form_for_tool( node.form_html + node.tooltip, node );
 703        }
 704    },
 705    layout : function () {
 706        this.check_changes_in_active_form();
 707        this.has_changes = true;
 708        // Prepare predecessor / successor tracking
 709        var n_pred = {};
 710        var successors = {};
 711        // First pass to initialize arrays even for nodes with no connections
 712        $.each( this.nodes, function( id, node ) {
 713            if ( n_pred[id] === undefined ) { n_pred[id] = 0; }
 714            if ( successors[id] === undefined ) { successors[id] = []; }
 715        });
 716        // Second pass to count predecessors and successors
 717        $.each( this.nodes, function( id, node ) {
 718            $.each( node.input_terminals, function ( j, t ) {
 719                $.each( t.connectors, function ( k, c ) {
 720                    // A connection exists from `other` to `node`
 721                    var other = c.handle1.node;
 722                    // node gains a predecessor
 723                    n_pred[node.id] += 1;
 724                    // other gains a successor
 725                    successors[other.id].push( node.id );
 726                });
 727            });
 728        });
 729        // Assemble order, tracking levels
 730        node_ids_by_level = [];
 731        while ( true ) {
 732            // Everything without a predecessor
 733            level_parents = [];
 734            for ( var pred_k in n_pred ) {
 735                if ( n_pred[ pred_k ] == 0 ) {
 736                    level_parents.push( pred_k );
 737                }
 738            }        
 739            if ( level_parents.length == 0 ) {
 740                break;
 741            }
 742            node_ids_by_level.push( level_parents );
 743            // Remove the parents from this level, and decrement the number
 744            // of predecessors for each successor
 745            for ( var k in level_parents ) {
 746                var v = level_parents[k];
 747                delete n_pred[v];
 748                for ( var sk in successors[v] ) {
 749                    n_pred[ successors[v][sk] ] -= 1;
 750                }
 751            }
 752        }
 753        if ( n_pred.length ) {
 754            // ERROR: CYCLE! Currently we do nothing
 755            return;
 756        }
 757        // Layout each level
 758        var all_nodes = this.nodes;
 759        var h_pad = 80; v_pad = 30;
 760        var left = h_pad;        
 761        $.each( node_ids_by_level, function( i, ids ) {
 762            // We keep nodes in the same order in a level to give the user
 763            // some control over ordering
 764            ids.sort( function( a, b ) {
 765                return $(all_nodes[a].element).position().top - $(all_nodes[b].element).position().top;
 766            });
 767            // Position each node
 768            var max_width = 0;
 769            var top = v_pad;
 770            $.each( ids, function( j, id ) {
 771                var node = all_nodes[id];
 772                var element = $(node.element);
 773                $(element).css( { top: top, left: left } );
 774                max_width = Math.max( max_width, $(element).width() );
 775                top += $(element).height() + v_pad;
 776            });
 777            left += max_width + h_pad;
 778        });
 779        // Need to redraw all connectors
 780        $.each( all_nodes, function( _, node ) { node.redraw(); } );
 781    },
 782    bounds_for_all_nodes: function() {
 783        var xmin = Infinity, xmax = -Infinity,
 784            ymin = Infinity, ymax = -Infinity,
 785            p;
 786        $.each( this.nodes, function( id, node ) {
 787            e = $(node.element);
 788            p = e.position();
 789            xmin = Math.min( xmin, p.left );
 790            xmax = Math.max( xmax, p.left + e.width() );
 791            ymin = Math.min( ymin, p.top );
 792            ymax = Math.max( ymax, p.top + e.width() );
 793        });
 794        return  { xmin: xmin, xmax: xmax, ymin: ymin, ymax: ymax };
 795    },
 796    fit_canvas_to_nodes: function() {
 797        // Span of all elements
 798        var bounds = this.bounds_for_all_nodes();
 799        var position = this.canvas_container.position();
 800        var parent = this.canvas_container.parent();
 801        // Determine amount we need to expand on top/left
 802        var xmin_delta = fix_delta( bounds.xmin, 100 );
 803        var ymin_delta = fix_delta( bounds.ymin, 100 );
 804        // May need to expand farther to fill viewport
 805        xmin_delta = Math.max( xmin_delta, position.left );
 806        ymin_delta = Math.max( ymin_delta, position.top );
 807        var left = position.left - xmin_delta;
 808        var top = position.top - ymin_delta;
 809        // Same for width/height
 810        var width = round_up( bounds.xmax + 100, 100 ) + xmin_delta;
 811        var height = round_up( bounds.ymax + 100, 100 ) + ymin_delta;
 812        width = Math.max( width, - left + parent.width() );
 813        height = Math.max( height, - top + parent.height() );
 814        // Grow the canvas container
 815        this.canvas_container.css( {
 816            left: left,
 817            top: top,
 818            width: width,
 819            height: height
 820        });
 821        // Move elements back if needed
 822        this.canvas_container.children().each( function() {
 823            var p = $(this).position();
 824            $(this).css( "left", p.left + xmin_delta );
 825            $(this).css( "top", p.top + ymin_delta );
 826        });
 827    }
 828});
 829
 830function fix_delta( x, n ) {
 831    if ( x < n|| x > 3*n ) {
 832        new_pos = ( Math.ceil( ( ( x % n ) ) / n ) + 1 ) * n;
 833        return ( - ( x - new_pos ) );
 834    }
 835    return 0;
 836}
 837    
 838function round_up( x, n ) {
 839    return Math.ceil( x / n ) * n;
 840}
 841     
 842function prebuild_node( type, title_text, tool_id ) {
 843    var f = $("<div class='toolForm toolFormInCanvas'></div>");
 844    var node = new Node( f );
 845    node.type = type;
 846    if ( type == 'tool' ) {
 847        node.tool_id = tool_id;
 848    }
 849    var title = $("<div class='toolFormTitle unselectable'>" + title_text + "</div>" );
 850    f.append( title );
 851    f.css( "left", $(window).scrollLeft() + 20 ); f.css( "top", $(window).scrollTop() + 20 );    
 852    var b = $("<div class='toolFormBody'></div>");
 853    var tmp = "<div><img height='16' align='middle' src='" + galaxy_config.root + "static/images/loading_small_white_bg.gif'/> loading tool info...</div>";
 854    b.append( tmp );
 855    node.form_html = tmp;
 856    f.append( b );
 857    // Fix width to computed width
 858    // Now add floats
 859    var buttons = $("<div class='buttons' style='float: right;'></div>");
 860    buttons.append( $("<div>").addClass("fa-icon-button fa fa-times").click( function( e ) {
 861        node.destroy();
 862    }));
 863    // Place inside container
 864    f.appendTo( "#canvas-container" );
 865    // Position in container
 866    var o = $("#canvas-container").position();
 867    var p = $("#canvas-container").parent();
 868    var width = f.width();
 869    var height = f.height();
 870    f.css( { left: ( - o.left ) + ( p.width() / 2 ) - ( width / 2 ), top: ( - o.top ) + ( p.height() / 2 ) - ( height / 2 ) } );
 871    buttons.prependTo( title );
 872    width += ( buttons.width() + 10 );
 873    f.css( "width", width );
 874    $(f).bind( "dragstart", function() {
 875        workflow.activate_node( node );
 876    }).bind( "dragend", function() {
 877        workflow.node_changed( this );
 878        workflow.fit_canvas_to_nodes();
 879        canvas_manager.draw_overview();
 880    }).bind( "dragclickonly", function() {
 881       workflow.activate_node( node ); 
 882    }).bind( "drag", function( e, d ) {
 883        // Move
 884        var po = $(this).offsetParent().offset(),
 885            x = d.offsetX - po.left,
 886            y = d.offsetY - po.top;
 887        $(this).css( { left: x, top: y } );
 888        // Redraw
 889        $(this).find( ".terminal" ).each( function() {
 890            this.terminal.redraw();
 891        });
 892    });
 893    return node;
 894}
 895
 896
 897var ext_to_type = null;
 898var type_to_type = null;
 899
 900function issubtype( child, parent ) {
 901    child = ext_to_type[child];
 902    parent = ext_to_type[parent];
 903    return ( type_to_type[child] ) && ( parent in type_to_type[child] );
 904}
 905
 906function populate_datatype_info( data ) {
 907    ext_to_type = data.ext_to_class_name;
 908    type_to_type = data.class_to_classes;
 909}
 910
 911// FIXME: merge scroll panel into CanvasManager, clean up hardcoded stuff.
 912
 913function ScrollPanel( panel ) {
 914    this.panel = panel;
 915}
 916$.extend( ScrollPanel.prototype, {
 917    test: function( e, onmove ) {
 918        clearTimeout( this.timeout );
 919        var x = e.pageX,
 920            y = e.pageY,
 921            // Panel size and position
 922            panel = $(this.panel),
 923            panel_pos = panel.position(),
 924            panel_w = panel.width(),
 925            panel_h = panel.height(),
 926            // Viewport size and offset
 927            viewport = panel.parent(),
 928            viewport_w = viewport.width(),
 929            viewport_h = viewport.height(),
 930            viewport_offset = viewport.offset(),
 931            // Edges of viewport (in page coordinates)
 932            min_x = viewport_offset.left,
 933            min_y = viewport_offset.top,
 934            max_x = min_x + viewport.width(),
 935            max_y = min_y + viewport.height(),
 936            // Legal panel range
 937            p_min_x = - ( panel_w - ( viewport_w / 2 ) ),
 938            p_min_y = - ( panel_h - ( viewport_h / 2 )),
 939            p_max_x = ( viewport_w / 2 ),
 940            p_max_y = ( viewport_h / 2 ),
 941            // Did the panel move?
 942            moved = false,
 943            // Constants
 944            close_dist = 5,
 945            nudge = 23;
 946        if ( x - close_dist < min_x ) {
 947            if ( panel_pos.left < p_max_x ) {
 948                var t = Math.min( nudge, p_max_x - panel_pos.left );
 949                panel.css( "left", panel_pos.left + t );
 950                moved = true;
 951            }
 952        } else if ( x + close_dist > max_x ) {
 953            if ( panel_pos.left > p_min_x ) {
 954                var t = Math.min( nudge, panel_pos.left  - p_min_x );
 955                panel.css( "left", panel_pos.left - t );
 956                moved = true;
 957            }
 958        } else if ( y - close_dist < min_y ) {
 959            if ( panel_pos.top < p_max_y ) {
 960                var t = Math.min( nudge, p_max_y - panel_pos.top );
 961                panel.css( "top", panel_pos.top + t );
 962                moved = true;
 963            }
 964        } else if ( y + close_dist > max_y ) {
 965            if ( panel_pos.top > p_min_y ) {
 966                var t = Math.min( nudge, panel_pos.top  - p_min_x );
 967                panel.css( "top", ( panel_pos.top - t ) + "px" );
 968                moved = true;
 969            }
 970        }
 971        if ( moved ) {
 972            // Keep moving even if mouse doesn't move
 973            onmove();
 974            var panel = this;
 975            this.timeout = setTimeout( function() { panel.test( e, onmove ); }, 50 );
 976        }
 977    },
 978    stop: function( e, ui ) {
 979        clearTimeout( this.timeout );
 980    }
 981});
 982
 983function CanvasManager( canvas_viewport, overview ) {
 984    this.cv = canvas_viewport;
 985    this.cc = this.cv.find( "#canvas-container" );
 986    this.oc = overview.find( "#overview-canvas" );
 987    this.ov = overview.find( "#overview-viewport" );
 988    // Make overview box draggable
 989    this.init_drag();
 990}
 991$.extend( CanvasManager.prototype, {
 992    init_drag : function () {
 993        var self = this;
 994        var move = function( x, y ) {
 995            x = Math.min( x, self.cv.width() / 2 );
 996            x = Math.max( x, - self.cc.width() + self.cv.width() / 2 );
 997            y = Math.min( y, self.cv.height() / 2 );
 998            y = Math.max( y, - self.cc.height() + self.cv.height() / 2 );
 999            self.cc.css( {
1000                left: x,
1001                top: y
1002            });
1003            self.update_viewport_overlay();
1004        };
1005        // Dragging within canvas background
1006        this.cc.each( function() {
1007            this.scroll_panel = new ScrollPanel( this );
1008        });
1009        var x_adjust, y_adjust;
1010        this.cv.bind( "dragstart", function() {
1011            var o = $(this).offset();
1012            var p = self.cc.position();
1013            y_adjust = p.top - o.top;
1014            x_adjust = p.left - o.left;
1015        }).bind( "drag", function( e, d ) {
1016            move( d.offsetX + x_adjust, d.offsetY + y_adjust );
1017        }).bind( "dragend", function() {
1018            workflow.fit_canvas_to_nodes();
1019            self.draw_overview();
1020        });
1021        // Dragging for overview pane
1022        this.ov.bind( "drag", function( e, d ) {
1023            var in_w = self.cc.width(),
1024                in_h = self.cc.height(),
1025                o_w = self.oc.width(),
1026                o_h = self.oc.height(),
1027                p = $(this).offsetParent().offset(),
1028                new_x_offset = d.offsetX - p.left,
1029                new_y_offset = d.offsetY - p.top;
1030            move( - ( new_x_offset / o_w * in_w ),
1031                  - ( new_y_offset / o_h * in_h ) );
1032        }).bind( "dragend", function() {
1033            workflow.fit_canvas_to_nodes();
1034            self.draw_overview();
1035        });
1036        // Dragging for overview border (resize)
1037        $("#overview-border").bind( "drag", function( e, d ) {
1038            var op = $(this).offsetParent();
1039            var opo = op.offset();
1040            var new_size = Math.max( op.width() - ( d.offsetX - opo.left ),
1041                                     op.height() - ( d.offsetY - opo.top ) );
1042            $(this).css( {
1043                width: new_size,
1044                height: new_size
1045            });
1046            self.draw_overview();
1047        });
1048        
1049        /*  Disable dragging for child element of the panel so that resizing can
1050            only be done by dragging the borders */
1051        $("#overview-border div").bind("drag", function() { });
1052        
1053    },
1054    update_viewport_overlay: function() {
1055        var cc = this.cc,
1056            cv = this.cv,
1057            oc = this.oc,
1058            ov = this.ov,
1059            in_w = cc.width(),
1060            in_h = cc.height(),
1061            o_w = oc.width(),
1062            o_h = oc.height(),
1063            cc_pos = cc.position();        
1064        ov.css( {
1065            left: - ( cc_pos.left / in_w * o_w ),
1066            top: - ( cc_pos.top / in_h * o_h ),
1067            // Subtract 2 to account for borders (maybe just change box sizing style instead?)
1068            width: ( cv.width() / in_w * o_w ) - 2,
1069            height: ( cv.height() / in_h * o_h ) - 2
1070        });
1071    },
1072    draw_overview: function() {
1073        var canvas_el = $("#overview-canvas"),
1074            size = canvas_el.parent().parent().width(),
1075            c = canvas_el.get(0).getContext("2d"),
1076            in_w = $("#canvas-container").width(),
1077            in_h = $("#canvas-container").height();
1078        var o_h, shift_h, o_w, shift_w;
1079        // Fit canvas into overview area
1080        var cv_w = this.cv.width();
1081        var cv_h = this.cv.height();
1082        if ( in_w < cv_w && in_h < cv_h ) {
1083            // Canvas is smaller than viewport
1084            o_w = in_w / cv_w * size;
1085            shift_w = ( size - o_w ) / 2;
1086            o_h = in_h / cv_h * size;
1087            shift_h = ( size - o_h ) / 2;
1088        } else if ( in_w < in_h ) {
1089            // Taller than wide
1090            shift_h = 0;
1091            o_h = size;
1092            o_w = Math.ceil( o_h * in_w / in_h );
1093            shift_w = ( size - o_w ) / 2;
1094        } else {
1095            // Wider than tall
1096            o_w = size;
1097            shift_w = 0;
1098            o_h = Math.ceil( o_w * in_h / in_w );
1099            shift_h = ( size - o_h ) / 2;
1100        }
1101        canvas_el.parent().css( {
1102           left: shift_w,
1103           top: shift_h,
1104           width: o_w,
1105           height: o_h
1106        });
1107        canvas_el.attr( "width", o_w );
1108        canvas_el.attr( "height", o_h );
1109        // Draw overview
1110        $.each( workflow.nodes, function( id, node ) {
1111            c.fillStyle = "#D2C099";
1112            c.strokeStyle = "#D8B365";
1113            c.lineWidth = 1;
1114            var node_element = $(node.element),
1115                position = node_element.position(),
1116                x = position.left / in_w * o_w,
1117                y = position.top / in_h * o_h,
1118                w = node_element.width() / in_w * o_w,
1119                h = node_element.height() / in_h * o_h;
1120            if (node.tool_errors){
1121                c.fillStyle = "#FFCCCC";
1122                c.strokeStyle = "#AA6666";
1123            } else if (node.workflow_outputs != undefined && node.workflow_outputs.length > 0){
1124                c.fillStyle = "#E8A92D";
1125                c.strokeStyle = "#E8A92D";
1126            }
1127            c.fillRect( x, y, w, h );
1128            c.strokeRect( x, y, w, h );
1129        });
1130        this.update_viewport_overlay();
1131    }
1132});