root/galaxy-central/static/scripts/galaxy.workflow_editor.canvas.js @ 2

リビジョン 2, 39.4 KB (コミッタ: hatakeyama, 14 年 前)

import galaxy-central

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