/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

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