PageRenderTime 58ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://bitbucket.org/remy_d1/galaxy-central-manageapi
JavaScript | 1991 lines | 1720 code | 130 blank | 141 comment | 241 complexity | c6f8619e1882812047aa9b91508c5d54 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. function CollectionTypeDescription( collectionType ) {
  2. this.collectionType = collectionType;
  3. this.isCollection = true;
  4. this.rank = collectionType.split(":").length;
  5. }
  6. $.extend( CollectionTypeDescription.prototype, {
  7. append: function( otherCollectionTypeDescription ) {
  8. if( otherCollectionTypeDescription === NULL_COLLECTION_TYPE_DESCRIPTION ) {
  9. return this;
  10. }
  11. if( otherCollectionTypeDescription === ANY_COLLECTION_TYPE_DESCRIPTION ) {
  12. return otherCollectionType;
  13. }
  14. return new CollectionTypeDescription( this.collectionType + ":" + otherCollectionTypeDescription.collectionType );
  15. },
  16. canMatch: function( otherCollectionTypeDescription ) {
  17. if( otherCollectionTypeDescription === NULL_COLLECTION_TYPE_DESCRIPTION ) {
  18. return false;
  19. }
  20. if( otherCollectionTypeDescription === ANY_COLLECTION_TYPE_DESCRIPTION ) {
  21. return true;
  22. }
  23. return otherCollectionTypeDescription.collectionType == this.collectionType;
  24. },
  25. canMapOver: function( otherCollectionTypeDescription ) {
  26. if( otherCollectionTypeDescription === NULL_COLLECTION_TYPE_DESCRIPTION ) {
  27. return false;
  28. }
  29. if( otherCollectionTypeDescription === ANY_COLLECTION_TYPE_DESCRIPTION ) {
  30. return false;
  31. }
  32. if( this.rank <= otherCollectionTypeDescription.rank ) {
  33. // Cannot map over self...
  34. return false;
  35. }
  36. var requiredSuffix = otherCollectionTypeDescription.collectionType
  37. return this._endsWith( this.collectionType, requiredSuffix );
  38. },
  39. effectiveMapOver: function( otherCollectionTypeDescription ) {
  40. var otherCollectionType = otherCollectionTypeDescription.collectionType;
  41. var effectiveCollectionType = this.collectionType.substring( 0, this.collectionType.length - otherCollectionType.length - 1 );
  42. return new CollectionTypeDescription( effectiveCollectionType );
  43. },
  44. equal: function( otherCollectionTypeDescription ) {
  45. return otherCollectionTypeDescription.collectionType == this.collectionType;
  46. },
  47. toString: function() {
  48. return "CollectionType[" + this.collectionType + "]";
  49. },
  50. _endsWith: function( str, suffix ) {
  51. return str.indexOf(suffix, str.length - suffix.length) !== -1;
  52. }
  53. } );
  54. NULL_COLLECTION_TYPE_DESCRIPTION = {
  55. isCollection: false,
  56. canMatch: function( other ) { return false; },
  57. canMapOver: function( other ) {
  58. return false;
  59. },
  60. toString: function() {
  61. return "NullCollectionType[]";
  62. },
  63. append: function( otherCollectionType ) {
  64. return otherCollectionType;
  65. },
  66. equal: function( other ) {
  67. return other === this;
  68. }
  69. };
  70. ANY_COLLECTION_TYPE_DESCRIPTION = {
  71. isCollection: true,
  72. canMatch: function( other ) { return NULL_COLLECTION_TYPE_DESCRIPTION !== other; },
  73. canMapOver: function( other ) {
  74. return false;
  75. },
  76. toString: function() {
  77. return "AnyCollectionType[]";
  78. },
  79. append: function( otherCollectionType ) {
  80. throw "Cannot append to ANY_COLLECTION_TYPE_DESCRIPTION";
  81. },
  82. equal: function( other ) {
  83. return other === this;
  84. }
  85. };
  86. var TerminalMapping = Backbone.Model.extend( {
  87. initialize: function( attr ) {
  88. this.mapOver = attr.mapOver || NULL_COLLECTION_TYPE_DESCRIPTION;
  89. this.terminal = attr.terminal;
  90. this.terminal.terminalMapping = this;
  91. },
  92. disableMapOver: function() {
  93. this.setMapOver( NULL_COLLECTION_TYPE_DESCRIPTION );
  94. },
  95. setMapOver: function( collectionTypeDescription ) {
  96. // TODO: Can I use "attributes" or something to auto trigger "change"
  97. // event?
  98. this.mapOver = collectionTypeDescription;
  99. this.trigger("change");
  100. }
  101. } );
  102. var TerminalMappingView = Backbone.View.extend( {
  103. tagName: "div",
  104. className: "fa-icon-button fa fa-folder-o",
  105. initialize: function( options ) {
  106. var mapText = "Run tool in parallel over collection";
  107. this.$el.tooltip( {delay: 500, title: mapText } );
  108. this.model.bind( "change", _.bind( this.render, this ) );
  109. },
  110. render: function() {
  111. if( this.model.mapOver.isCollection ) {
  112. this.$el.show();
  113. } else {
  114. this.$el.hide();
  115. }
  116. },
  117. } );
  118. var InputTerminalMappingView = TerminalMappingView.extend( {
  119. events: {
  120. "click": "onClick",
  121. "mouseenter": "onMouseEnter",
  122. "mouseleave": "onMouseLeave",
  123. },
  124. onMouseEnter: function( e ) {
  125. var model = this.model;
  126. if( ! model.terminal.connected() && model.mapOver.isCollection ) {
  127. this.$el.css( "color", "red" );
  128. }
  129. },
  130. onMouseLeave: function( e ) {
  131. this.$el.css( "color", "black" );
  132. },
  133. onClick: function( e ) {
  134. var model = this.model;
  135. if( ! model.terminal.connected() && model.mapOver.isCollection ) {
  136. // TODO: Consider prompting...
  137. model.terminal.resetMapping();
  138. }
  139. },
  140. } );
  141. var InputTerminalMapping = TerminalMapping;
  142. var InputCollectionTerminalMapping = TerminalMapping;
  143. var OutputTerminalMapping = TerminalMapping;
  144. var OutputTerminalMappingView = TerminalMappingView;
  145. var InputCollectionTerminalMappingView = InputTerminalMappingView;
  146. var OutputCollectionTerminalMapping = TerminalMapping;
  147. var OutputCollectionTerminalMappingView = TerminalMappingView;
  148. var Terminal = Backbone.Model.extend( {
  149. initialize: function( attr ) {
  150. this.element = attr.element;
  151. this.connectors = [];
  152. },
  153. connect: function ( connector ) {
  154. this.connectors.push( connector );
  155. if ( this.node ) {
  156. this.node.markChanged();
  157. }
  158. },
  159. disconnect: function ( connector ) {
  160. this.connectors.splice( $.inArray( connector, this.connectors ), 1 );
  161. if ( this.node ) {
  162. this.node.markChanged();
  163. this.resetMappingIfNeeded();
  164. }
  165. },
  166. redraw: function () {
  167. $.each( this.connectors, function( _, c ) {
  168. c.redraw();
  169. });
  170. },
  171. destroy: function () {
  172. $.each( this.connectors.slice(), function( _, c ) {
  173. c.destroy();
  174. });
  175. },
  176. destroyInvalidConnections: function( ) {
  177. _.each( this.connectors, function( connector ) {
  178. connector.destroyIfInvalid();
  179. } );
  180. },
  181. setMapOver : function( val ) {
  182. if( this.multiple ) {
  183. return; // Cannot set this to be multirun...
  184. }
  185. if( ! this.mapOver().equal( val ) ) {
  186. this.terminalMapping.setMapOver( val );
  187. _.each( this.node.output_terminals, function( outputTerminal ) {
  188. outputTerminal.setMapOver( val );
  189. } );
  190. }
  191. },
  192. mapOver: function( ) {
  193. if ( ! this.terminalMapping ) {
  194. return NULL_COLLECTION_TYPE_DESCRIPTION;
  195. } else {
  196. return this.terminalMapping.mapOver;
  197. }
  198. },
  199. isMappedOver: function( ) {
  200. return this.terminalMapping && this.terminalMapping.mapOver.isCollection;
  201. },
  202. resetMapping: function() {
  203. this.terminalMapping.disableMapOver();
  204. },
  205. resetMappingIfNeeded: function( ) {}, // Subclasses should override this...
  206. } );
  207. var OutputTerminal = Terminal.extend( {
  208. initialize: function( attr ) {
  209. Terminal.prototype.initialize.call( this, attr );
  210. this.datatypes = attr.datatypes;
  211. },
  212. resetMappingIfNeeded: function( ) {
  213. // If inputs were only mapped over to preserve
  214. // an output just disconnected reset these...
  215. if( ! this.node.hasConnectedOutputTerminals() && ! this.node.hasConnectedMappedInputTerminals()){
  216. _.each( this.node.mappedInputTerminals(), function( mappedInput ) {
  217. mappedInput.resetMappingIfNeeded();
  218. } );
  219. }
  220. var noMappedInputs = ! this.node.hasMappedOverInputTerminals();
  221. if( noMappedInputs ) {
  222. this.resetMapping();
  223. }
  224. },
  225. resetMapping: function() {
  226. this.terminalMapping.disableMapOver();
  227. _.each( this.connectors, function( connector ) {
  228. var connectedInput = connector.handle2;
  229. if( connectedInput ) {
  230. // Not exactly right because this is still connected.
  231. // Either rewrite resetMappingIfNeeded or disconnect
  232. // and reconnect if valid.
  233. connectedInput.resetMappingIfNeeded();
  234. connector.destroyIfInvalid();
  235. }
  236. } );
  237. }
  238. } );
  239. var BaseInputTerminal = Terminal.extend( {
  240. initialize: function( attr ) {
  241. Terminal.prototype.initialize.call( this, attr );
  242. this.update( attr.input ); // subclasses should implement this...
  243. },
  244. canAccept: function ( other ) {
  245. if( this._inputFilled() ) {
  246. return false;
  247. } else {
  248. return this.attachable( other );
  249. }
  250. },
  251. resetMappingIfNeeded: function( ) {
  252. var mapOver = this.mapOver();
  253. if( ! mapOver.isCollection ) {
  254. return;
  255. }
  256. // No output terminals are counting on this being mapped
  257. // over if connected inputs are still mapped over or if none
  258. // of the outputs are connected...
  259. var reset = this.node.hasConnectedMappedInputTerminals() ||
  260. ( ! this.node.hasConnectedOutputTerminals() );
  261. if( reset ) {
  262. this.resetMapping();
  263. }
  264. },
  265. resetMapping: function() {
  266. this.terminalMapping.disableMapOver();
  267. if( ! this.node.hasMappedOverInputTerminals() ) {
  268. _.each( this.node.output_terminals, function( terminal) {
  269. // This shouldn't be called if there are mapped over
  270. // outputs.
  271. terminal.resetMapping();
  272. } );
  273. }
  274. },
  275. connected: function() {
  276. return this.connectors.length !== 0;
  277. },
  278. _inputFilled: function() {
  279. var inputFilled;
  280. if( ! this.connected() ) {
  281. inputFilled = false;
  282. } else {
  283. if( this.multiple ) {
  284. if(this._collectionAttached()) {
  285. // Can only attach one collection to multiple input
  286. // data parameter.
  287. inputsFilled = true;
  288. } else {
  289. inputFilled = false;
  290. }
  291. } else {
  292. inputFilled = true;
  293. }
  294. }
  295. return inputFilled;
  296. },
  297. _collectionAttached: function( ) {
  298. if( ! this.connected() ) {
  299. return false;
  300. } else {
  301. var firstOutput = this.connectors[ 0 ].handle1;
  302. if( ! firstOutput ){
  303. return false;
  304. } else {
  305. if( firstOutput.isDataCollectionInput || firstOutput.isMappedOver() || firstOutput.datatypes.indexOf( "input_collection" ) > 0 ) {
  306. return true;
  307. } else {
  308. return false;
  309. }
  310. }
  311. }
  312. },
  313. _mappingConstraints: function( ) {
  314. // If this is a connected terminal, return list of collection types
  315. // other terminals connected to node are constraining mapping to.
  316. if( ! this.node ) {
  317. return []; // No node - completely unconstrained
  318. }
  319. var mapOver = this.mapOver();
  320. if( mapOver.isCollection ) {
  321. return [ mapOver ];
  322. }
  323. var constraints = [];
  324. if( ! this.node.hasConnectedOutputTerminals() ) {
  325. _.each( this.node.connectedMappedInputTerminals(), function( inputTerminal ) {
  326. constraints.push( inputTerminal.mapOver() );
  327. } );
  328. } else {
  329. // All outputs should have same mapOver status - least specific.
  330. constraints.push( _.first( _.values( this.node.output_terminals ) ).mapOver() );
  331. }
  332. return constraints;
  333. },
  334. _producesAcceptableDatatype: function( other ) {
  335. // other is a non-collection output...
  336. for ( var t in this.datatypes ) {
  337. var cat_outputs = new Array();
  338. cat_outputs = cat_outputs.concat(other.datatypes);
  339. if (other.node.post_job_actions){
  340. for (var pja_i in other.node.post_job_actions){
  341. var pja = other.node.post_job_actions[pja_i];
  342. if (pja.action_type == "ChangeDatatypeAction" && (pja.output_name == '' || pja.output_name == other.name) && pja.action_arguments){
  343. cat_outputs.push(pja.action_arguments['newtype']);
  344. }
  345. }
  346. }
  347. // FIXME: No idea what to do about case when datatype is 'input'
  348. for ( var other_datatype_i in cat_outputs ) {
  349. var other_datatype = cat_outputs[other_datatype_i];
  350. if ( other_datatype == "input" || other_datatype == "input_collection" || issubtype( cat_outputs[other_datatype_i], this.datatypes[t] ) ) {
  351. return true;
  352. }
  353. }
  354. }
  355. return false;
  356. },
  357. _otherCollectionType: function( other ) {
  358. var otherCollectionType = NULL_COLLECTION_TYPE_DESCRIPTION;
  359. if( other.isDataCollectionInput ) {
  360. otherCollectionType = other.collectionType;
  361. } else {
  362. var otherMapOver = other.mapOver();
  363. if( otherMapOver.isCollection ) {
  364. otherCollectionType = otherMapOver;
  365. }
  366. }
  367. return otherCollectionType;
  368. },
  369. } );
  370. var InputTerminal = BaseInputTerminal.extend( {
  371. update: function( input ) {
  372. this.datatypes = input.extensions;
  373. this.multiple = input.multiple;
  374. this.collection = false;
  375. },
  376. connect: function( connector ) {
  377. BaseInputTerminal.prototype.connect.call( this, connector );
  378. var other_output = connector.handle1;
  379. if( ! other_output ) {
  380. return;
  381. }
  382. var otherCollectionType = this._otherCollectionType( other_output );
  383. if( otherCollectionType.isCollection ) {
  384. this.setMapOver( otherCollectionType );
  385. }
  386. },
  387. attachable: function( other ) {
  388. var otherCollectionType = this._otherCollectionType( other );
  389. var thisMapOver = this.mapOver();
  390. if( otherCollectionType.isCollection ) {
  391. if( this.multiple ) {
  392. if( this.connected() && ! this._collectionAttached() ) {
  393. // if single inputs attached, cannot also attach a
  394. // collection (yet...)
  395. return false;
  396. }
  397. if( otherCollectionType.rank == 1 ) {
  398. return this._producesAcceptableDatatype( other );
  399. } else {
  400. // TODO: Allow subcollection mapping over this as if it were
  401. // a list collection input.
  402. return false;
  403. }
  404. }
  405. if( thisMapOver.isCollection && thisMapOver.canMatch( otherCollectionType ) ) {
  406. return this._producesAcceptableDatatype( other );
  407. } else {
  408. // Need to check if this would break constraints...
  409. var mappingConstraints = this._mappingConstraints();
  410. if( mappingConstraints.every( _.bind( otherCollectionType.canMatch, otherCollectionType ) ) ) {
  411. return this._producesAcceptableDatatype( other );
  412. } else {
  413. return false;
  414. }
  415. }
  416. } else if( thisMapOver.isCollection ) {
  417. // Attempting to match a non-collection output to an
  418. // explicitly collection input.
  419. return false;
  420. }
  421. return this._producesAcceptableDatatype( other );
  422. }
  423. });
  424. var InputCollectionTerminal = BaseInputTerminal.extend( {
  425. update: function( input ) {
  426. this.multiple = false;
  427. this.collection = true;
  428. this.datatypes = input.extensions;
  429. if( input.collection_type ) {
  430. this.collectionType = new CollectionTypeDescription( input.collection_type );
  431. } else {
  432. this.collectionType = ANY_COLLECTION_TYPE_DESCRIPTION;
  433. }
  434. },
  435. connect: function( connector ) {
  436. BaseInputTerminal.prototype.connect.call( this, connector );
  437. var other = connector.handle1;
  438. if( ! other ) {
  439. return;
  440. }
  441. var effectiveMapOver = this._effectiveMapOver( other );
  442. this.setMapOver( effectiveMapOver );
  443. },
  444. _effectiveMapOver: function( other ) {
  445. var collectionType = this.collectionType;
  446. var otherCollectionType = this._otherCollectionType( other );
  447. if( ! collectionType.canMatch( otherCollectionType ) ) {
  448. return otherCollectionType.effectiveMapOver( collectionType );
  449. } else {
  450. return NULL_COLLECTION_TYPE_DESCRIPTION;
  451. }
  452. },
  453. _effectiveCollectionType: function( ) {
  454. var collectionType = this.collectionType;
  455. var thisMapOver = this.mapOver();
  456. return thisMapOver.append( collectionType );
  457. },
  458. attachable: function ( other ) {
  459. var otherCollectionType = this._otherCollectionType( other );
  460. if( otherCollectionType.isCollection ) {
  461. var effectiveCollectionType = this._effectiveCollectionType( );
  462. var thisMapOver = this.mapOver();
  463. if( effectiveCollectionType.canMatch( otherCollectionType ) ) {
  464. // Only way a direct match...
  465. return this._producesAcceptableDatatype( other );
  466. // Otherwise we need to mapOver
  467. } else if( thisMapOver.isCollection ) {
  468. // In this case, mapOver already set and we didn't match skipping...
  469. return false;
  470. } else if( otherCollectionType.canMapOver( this.collectionType ) ) {
  471. var effectiveMapOver = this._effectiveMapOver( other );
  472. if( ! effectiveMapOver.isCollection ) {
  473. return false;
  474. }
  475. // Need to check if this would break constraints...
  476. var mappingConstraints = this._mappingConstraints();
  477. if( mappingConstraints.every( effectiveMapOver.canMatch ) ) {
  478. return this._producesAcceptableDatatype( other );
  479. }
  480. }
  481. }
  482. return false;
  483. }
  484. });
  485. var OutputCollectionTerminal = Terminal.extend( {
  486. initialize: function( attr ) {
  487. Terminal.prototype.initialize.call( this, attr );
  488. this.datatypes = attr.datatypes;
  489. this.collectionType = new CollectionTypeDescription( attr.collection_type );
  490. this.isDataCollectionInput = true;
  491. },
  492. update: function( output ) {
  493. var newCollectionType = new CollectionTypeDescription( output.collection_type );
  494. if( newCollectionType.collectionType != this.collectionType.collectionType ) {
  495. _.each( this.connectors, function( connector ) {
  496. // TODO: consider checking if connection valid before removing...
  497. connector.destroy();
  498. } );
  499. }
  500. this.collectionType = newCollectionType;
  501. }
  502. } );
  503. function Connector( handle1, handle2 ) {
  504. this.canvas = null;
  505. this.dragging = false;
  506. this.inner_color = "#FFFFFF";
  507. this.outer_color = "#D8B365";
  508. if ( handle1 && handle2 ) {
  509. this.connect( handle1, handle2 );
  510. }
  511. }
  512. $.extend( Connector.prototype, {
  513. connect: function ( t1, t2 ) {
  514. this.handle1 = t1;
  515. if ( this.handle1 ) {
  516. this.handle1.connect( this );
  517. }
  518. this.handle2 = t2;
  519. if ( this.handle2 ) {
  520. this.handle2.connect( this );
  521. }
  522. },
  523. destroy : function () {
  524. if ( this.handle1 ) {
  525. this.handle1.disconnect( this );
  526. }
  527. if ( this.handle2 ) {
  528. this.handle2.disconnect( this );
  529. }
  530. $(this.canvas).remove();
  531. },
  532. destroyIfInvalid: function() {
  533. if( this.handle1 && this.handle2 && ! this.handle2.attachable( this.handle1 ) ) {
  534. this.destroy();
  535. }
  536. },
  537. redraw : function () {
  538. var canvas_container = $("#canvas-container");
  539. if ( ! this.canvas ) {
  540. this.canvas = document.createElement( "canvas" );
  541. // excanvas specific hack
  542. if ( window.G_vmlCanvasManager ) {
  543. G_vmlCanvasManager.initElement( this.canvas );
  544. }
  545. canvas_container.append( $(this.canvas) );
  546. if ( this.dragging ) {
  547. this.canvas.style.zIndex = "300";
  548. }
  549. }
  550. var relativeLeft = function( e ) {
  551. return $(e).offset().left - canvas_container.offset().left;
  552. };
  553. var relativeTop = function( e ) {
  554. return $(e).offset().top - canvas_container.offset().top;
  555. };
  556. if (!this.handle1 || !this.handle2) {
  557. return;
  558. }
  559. // Find the position of each handle
  560. var start_x = relativeLeft( this.handle1.element ) + 5;
  561. var start_y = relativeTop( this.handle1.element ) + 5;
  562. var end_x = relativeLeft( this.handle2.element ) + 5;
  563. var end_y = relativeTop( this.handle2.element ) + 5;
  564. // Calculate canvas area
  565. var canvas_extra = 100;
  566. var canvas_min_x = Math.min( start_x, end_x );
  567. var canvas_max_x = Math.max( start_x, end_x );
  568. var canvas_min_y = Math.min( start_y, end_y );
  569. var canvas_max_y = Math.max( start_y, end_y );
  570. var cp_shift = Math.min( Math.max( Math.abs( canvas_max_y - canvas_min_y ) / 2, 100 ), 300 );
  571. var canvas_left = canvas_min_x - canvas_extra;
  572. var canvas_top = canvas_min_y - canvas_extra;
  573. var canvas_width = canvas_max_x - canvas_min_x + 2 * canvas_extra;
  574. var canvas_height = canvas_max_y - canvas_min_y + 2 * canvas_extra;
  575. // Place the canvas
  576. this.canvas.style.left = canvas_left + "px";
  577. this.canvas.style.top = canvas_top + "px";
  578. this.canvas.setAttribute( "width", canvas_width );
  579. this.canvas.setAttribute( "height", canvas_height );
  580. // Adjust points to be relative to the canvas
  581. start_x -= canvas_left;
  582. start_y -= canvas_top;
  583. end_x -= canvas_left;
  584. end_y -= canvas_top;
  585. // Draw the line
  586. var c = this.canvas.getContext("2d"),
  587. start_offsets = null,
  588. end_offsets = null;
  589. var num_offsets = 1;
  590. if ( this.handle1 && this.handle1.isMappedOver() ) {
  591. var start_offsets = [ -6, -3, 0, 3, 6 ];
  592. num_offsets = 5;
  593. } else {
  594. var start_offsets = [ 0 ];
  595. }
  596. if ( this.handle2 && this.handle2.isMappedOver() ) {
  597. var end_offsets = [ -6, -3, 0, 3, 6 ];
  598. num_offsets = 5;
  599. } else {
  600. var end_offsets = [ 0 ];
  601. }
  602. var connector = this;
  603. for( var i = 0; i < num_offsets; i++ ) {
  604. var inner_width = 5,
  605. outer_width = 7;
  606. if( start_offsets.length > 1 || end_offsets.length > 1 ) {
  607. // We have a multi-run, using many lines, make them small.
  608. inner_width = 1;
  609. outer_width = 3;
  610. }
  611. connector.draw_outlined_curve( start_x, start_y, end_x, end_y, cp_shift, inner_width, outer_width, start_offsets[ i % start_offsets.length ], end_offsets[ i % end_offsets.length ] );
  612. }
  613. },
  614. draw_outlined_curve : function( start_x, start_y, end_x, end_y, cp_shift, inner_width, outer_width, offset_start, offset_end ) {
  615. var offset_start = offset_start || 0;
  616. var offset_end = offset_end || 0;
  617. var c = this.canvas.getContext("2d");
  618. c.lineCap = "round";
  619. c.strokeStyle = this.outer_color;
  620. c.lineWidth = outer_width;
  621. c.beginPath();
  622. c.moveTo( start_x, start_y + offset_start );
  623. c.bezierCurveTo( start_x + cp_shift, start_y + offset_start, end_x - cp_shift, end_y + offset_end, end_x, end_y + offset_end);
  624. c.stroke();
  625. // Inner line
  626. c.strokeStyle = this.inner_color;
  627. c.lineWidth = inner_width;
  628. c.beginPath();
  629. c.moveTo( start_x, start_y + offset_start );
  630. c.bezierCurveTo( start_x + cp_shift, start_y + offset_start, end_x - cp_shift, end_y + offset_end, end_x, end_y + offset_end );
  631. c.stroke();
  632. }
  633. } );
  634. var Node = Backbone.Model.extend({
  635. initialize: function( attr ) {
  636. this.element = attr.element;
  637. this.input_terminals = {};
  638. this.output_terminals = {};
  639. this.tool_errors = {};
  640. },
  641. connectedOutputTerminals: function() {
  642. return this._connectedTerminals( this.output_terminals );
  643. },
  644. _connectedTerminals: function( terminals ) {
  645. var connectedTerminals = [];
  646. $.each( terminals, function( _, t ) {
  647. if( t.connectors.length > 0 ) {
  648. connectedTerminals.push( t );
  649. }
  650. } );
  651. return connectedTerminals;
  652. },
  653. hasConnectedOutputTerminals: function() {
  654. // return this.connectedOutputTerminals().length > 0; <- optimized this
  655. var outputTerminals = this.output_terminals;
  656. for( var outputName in outputTerminals ) {
  657. if( outputTerminals[ outputName ].connectors.length > 0 ) {
  658. return true;
  659. }
  660. }
  661. return false;
  662. },
  663. connectedMappedInputTerminals: function() {
  664. return this._connectedMappedTerminals( this.input_terminals );
  665. },
  666. hasConnectedMappedInputTerminals: function() {
  667. // return this.connectedMappedInputTerminals().length > 0; <- optimized this
  668. var inputTerminals = this.input_terminals;
  669. for( var inputName in inputTerminals ) {
  670. var inputTerminal = inputTerminals[ inputName ];
  671. if( inputTerminal.connectors.length > 0 && inputTerminal.isMappedOver() ) {
  672. return true;
  673. }
  674. }
  675. return false;
  676. },
  677. _connectedMappedTerminals: function( terminals ) {
  678. var mapped_outputs = [];
  679. $.each( terminals, function( _, t ) {
  680. var mapOver = t.mapOver();
  681. if( mapOver.isCollection ) {
  682. if( t.connectors.length > 0 ) {
  683. mapped_outputs.push( t );
  684. }
  685. }
  686. });
  687. return mapped_outputs;
  688. },
  689. mappedInputTerminals: function() {
  690. return this._mappedTerminals( this.input_terminals );
  691. },
  692. _mappedTerminals: function( terminals ) {
  693. var mappedTerminals = [];
  694. $.each( terminals, function( _, t ) {
  695. var mapOver = t.mapOver();
  696. if( mapOver.isCollection ) {
  697. mappedTerminals.push( t );
  698. }
  699. } );
  700. return mappedTerminals;
  701. },
  702. hasMappedOverInputTerminals: function() {
  703. var found = false;
  704. _.each( this.input_terminals, function( t ) {
  705. var mapOver = t.mapOver();
  706. if( mapOver.isCollection ) {
  707. found = true;
  708. }
  709. } );
  710. return found;
  711. },
  712. redraw : function () {
  713. $.each( this.input_terminals, function( _, t ) {
  714. t.redraw();
  715. });
  716. $.each( this.output_terminals, function( _, t ) {
  717. t.redraw();
  718. });
  719. },
  720. destroy : function () {
  721. $.each( this.input_terminals, function( k, t ) {
  722. t.destroy();
  723. });
  724. $.each( this.output_terminals, function( k, t ) {
  725. t.destroy();
  726. });
  727. workflow.remove_node( this );
  728. $(this.element).remove();
  729. },
  730. make_active : function () {
  731. $(this.element).addClass( "toolForm-active" );
  732. },
  733. make_inactive : function () {
  734. // Keep inactive nodes stacked from most to least recently active
  735. // by moving element to the end of parent's node list
  736. var element = this.element.get(0);
  737. (function(p) { p.removeChild( element ); p.appendChild( element ); })(element.parentNode);
  738. // Remove active class
  739. $(element).removeClass( "toolForm-active" );
  740. },
  741. init_field_data : function ( data ) {
  742. if ( data.type ) {
  743. this.type = data.type;
  744. }
  745. this.name = data.name;
  746. this.form_html = data.form_html;
  747. this.tool_state = data.tool_state;
  748. this.tool_errors = data.tool_errors;
  749. this.tooltip = data.tooltip ? data.tooltip : "";
  750. this.annotation = data.annotation;
  751. this.post_job_actions = data.post_job_actions ? data.post_job_actions : {};
  752. this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : [];
  753. var node = this;
  754. var nodeView = new NodeView({
  755. el: this.element[ 0 ],
  756. node: node,
  757. });
  758. node.nodeView = nodeView;
  759. $.each( data.data_inputs, function( i, input ) {
  760. nodeView.addDataInput( input );
  761. });
  762. if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) {
  763. nodeView.addRule();
  764. }
  765. $.each( data.data_outputs, function( i, output ) {
  766. nodeView.addDataOutput( output );
  767. } );
  768. nodeView.render();
  769. workflow.node_changed( this );
  770. },
  771. update_field_data : function( data ) {
  772. var node = this;
  773. nodeView = node.nodeView;
  774. this.tool_state = data.tool_state;
  775. this.form_html = data.form_html;
  776. this.tool_errors = data.tool_errors;
  777. this.annotation = data['annotation'];
  778. if( "post_job_actions" in data ) {
  779. // Won't be present in response for data inputs
  780. var pja_in = $.parseJSON(data.post_job_actions);
  781. this.post_job_actions = pja_in ? pja_in : {};
  782. }
  783. node.nodeView.renderToolErrors();
  784. // Update input rows
  785. var old_body = nodeView.$( "div.inputs" );
  786. var new_body = nodeView.newInputsDiv();
  787. var newTerminalViews = {};
  788. _.each( data.data_inputs, function( input ) {
  789. var terminalView = node.nodeView.addDataInput( input, new_body );
  790. newTerminalViews[ input.name ] = terminalView;
  791. });
  792. // Cleanup any leftover terminals
  793. _.each( _.difference( _.values( nodeView.terminalViews ), _.values( newTerminalViews ) ), function( unusedView ) {
  794. unusedView.el.terminal.destroy();
  795. } );
  796. nodeView.terminalViews = newTerminalViews;
  797. // In general workflow editor assumes tool outputs don't change in # or
  798. // type (not really valid right?) but adding special logic here for
  799. // data collection input parameters that can have their collection
  800. // change.
  801. if( data.data_outputs.length == 1 && "collection_type" in data.data_outputs[ 0 ] ) {
  802. nodeView.updateDataOutput( data.data_outputs[ 0 ] );
  803. }
  804. old_body.replaceWith( new_body );
  805. // If active, reactivate with new form_html
  806. this.markChanged();
  807. this.redraw();
  808. },
  809. error : function ( text ) {
  810. var b = $(this.element).find( ".toolFormBody" );
  811. b.find( "div" ).remove();
  812. var tmp = "<div style='color: red; text-style: italic;'>" + text + "</div>";
  813. this.form_html = tmp;
  814. b.html( tmp );
  815. workflow.node_changed( this );
  816. },
  817. markChanged: function() {
  818. workflow.node_changed( this );
  819. }
  820. } );
  821. function Workflow( canvas_container ) {
  822. this.canvas_container = canvas_container;
  823. this.id_counter = 0;
  824. this.nodes = {};
  825. this.name = null;
  826. this.has_changes = false;
  827. this.active_form_has_changes = false;
  828. }
  829. $.extend( Workflow.prototype, {
  830. add_node : function( node ) {
  831. node.id = this.id_counter;
  832. node.element.attr( 'id', 'wf-node-step-' + node.id );
  833. this.id_counter++;
  834. this.nodes[ node.id ] = node;
  835. this.has_changes = true;
  836. node.workflow = this;
  837. },
  838. remove_node : function( node ) {
  839. if ( this.active_node == node ) {
  840. this.clear_active_node();
  841. }
  842. delete this.nodes[ node.id ] ;
  843. this.has_changes = true;
  844. },
  845. remove_all : function() {
  846. wf = this;
  847. $.each( this.nodes, function ( k, v ) {
  848. v.destroy();
  849. wf.remove_node( v );
  850. });
  851. },
  852. rectify_workflow_outputs : function() {
  853. // Find out if we're using workflow_outputs or not.
  854. var using_workflow_outputs = false;
  855. var has_existing_pjas = false;
  856. $.each( this.nodes, function ( k, node ) {
  857. if (node.workflow_outputs && node.workflow_outputs.length > 0){
  858. using_workflow_outputs = true;
  859. }
  860. $.each(node.post_job_actions, function(pja_id, pja){
  861. if (pja.action_type === "HideDatasetAction"){
  862. has_existing_pjas = true;
  863. }
  864. });
  865. });
  866. if (using_workflow_outputs !== false || has_existing_pjas !== false){
  867. // Using workflow outputs, or has existing pjas. Remove all PJAs and recreate based on outputs.
  868. $.each(this.nodes, function (k, node ){
  869. if (node.type === 'tool'){
  870. var node_changed = false;
  871. if (node.post_job_actions == null){
  872. node.post_job_actions = {};
  873. node_changed = true;
  874. }
  875. var pjas_to_rem = [];
  876. $.each(node.post_job_actions, function(pja_id, pja){
  877. if (pja.action_type == "HideDatasetAction"){
  878. pjas_to_rem.push(pja_id);
  879. }
  880. });
  881. if (pjas_to_rem.length > 0 ) {
  882. $.each(pjas_to_rem, function(i, pja_name){
  883. node_changed = true;
  884. delete node.post_job_actions[pja_name];
  885. });
  886. }
  887. if (using_workflow_outputs){
  888. $.each(node.output_terminals, function(ot_id, ot){
  889. var create_pja = true;
  890. $.each(node.workflow_outputs, function(i, wo_name){
  891. if (ot.name === wo_name){
  892. create_pja = false;
  893. }
  894. });
  895. if (create_pja === true){
  896. node_changed = true;
  897. var pja = {
  898. action_type : "HideDatasetAction",
  899. output_name : ot.name,
  900. action_arguments : {}
  901. }
  902. node.post_job_actions['HideDatasetAction'+ot.name] = null;
  903. node.post_job_actions['HideDatasetAction'+ot.name] = pja;
  904. }
  905. });
  906. }
  907. // lastly, if this is the active node, and we made changes, reload the display at right.
  908. if (workflow.active_node == node && node_changed === true) {
  909. workflow.reload_active_node();
  910. }
  911. }
  912. });
  913. }
  914. },
  915. to_simple : function () {
  916. var nodes = {};
  917. $.each( this.nodes, function ( i, node ) {
  918. var input_connections = {};
  919. $.each( node.input_terminals, function ( k, t ) {
  920. input_connections[ t.name ] = null;
  921. // There should only be 0 or 1 connectors, so this is
  922. // really a sneaky if statement
  923. var cons = []
  924. $.each( t.connectors, function ( i, c ) {
  925. cons[i] = { id: c.handle1.node.id, output_name: c.handle1.name };
  926. input_connections[ t.name ] = cons;
  927. });
  928. });
  929. var post_job_actions = {};
  930. if (node.post_job_actions){
  931. $.each( node.post_job_actions, function ( i, act ) {
  932. var pja = {
  933. action_type : act.action_type,
  934. output_name : act.output_name,
  935. action_arguments : act.action_arguments
  936. }
  937. post_job_actions[ act.action_type + act.output_name ] = null;
  938. post_job_actions[ act.action_type + act.output_name ] = pja;
  939. });
  940. }
  941. if (!node.workflow_outputs){
  942. node.workflow_outputs = [];
  943. // Just in case.
  944. }
  945. var node_data = {
  946. id : node.id,
  947. type : node.type,
  948. tool_id : node.tool_id,
  949. tool_state : node.tool_state,
  950. tool_errors : node.tool_errors,
  951. input_connections : input_connections,
  952. position : $(node.element).position(),
  953. annotation: node.annotation,
  954. post_job_actions: node.post_job_actions,
  955. workflow_outputs: node.workflow_outputs
  956. };
  957. nodes[ node.id ] = node_data;
  958. });
  959. return { steps: nodes };
  960. },
  961. from_simple : function ( data ) {
  962. wf = this;
  963. var max_id = 0;
  964. wf.name = data.name;
  965. // First pass, nodes
  966. var using_workflow_outputs = false;
  967. $.each( data.steps, function( id, step ) {
  968. var node = prebuild_node( step.type, step.name, step.tool_id );
  969. node.init_field_data( step );
  970. if ( step.position ) {
  971. node.element.css( { top: step.position.top, left: step.position.left } );
  972. }
  973. node.id = step.id;
  974. wf.nodes[ node.id ] = node;
  975. max_id = Math.max( max_id, parseInt( id ) );
  976. // For older workflows, it's possible to have HideDataset PJAs, but not WorkflowOutputs.
  977. // Check for either, and then add outputs in the next pass.
  978. if (!using_workflow_outputs && node.type === 'tool'){
  979. if (node.workflow_outputs.length > 0){
  980. using_workflow_outputs = true;
  981. }
  982. else{
  983. $.each(node.post_job_actions, function(pja_id, pja){
  984. if (pja.action_type === "HideDatasetAction"){
  985. using_workflow_outputs = true;
  986. }
  987. });
  988. }
  989. }
  990. });
  991. wf.id_counter = max_id + 1;
  992. // Second pass, connections
  993. $.each( data.steps, function( id, step ) {
  994. var node = wf.nodes[id];
  995. $.each( step.input_connections, function( k, v ) {
  996. if ( v ) {
  997. if ( ! $.isArray( v ) ) {
  998. v = [ v ];
  999. }
  1000. $.each( v, function( l, x ) {
  1001. var other_node = wf.nodes[ x.id ];
  1002. var c = new Connector();
  1003. c.connect( other_node.output_terminals[ x.output_name ],
  1004. node.input_terminals[ k ] );
  1005. c.redraw();
  1006. });
  1007. }
  1008. });
  1009. if(using_workflow_outputs && node.type === 'tool'){
  1010. // Ensure that every output terminal has a WorkflowOutput or HideDatasetAction.
  1011. $.each(node.output_terminals, function(ot_id, ot){
  1012. if(node.post_job_actions['HideDatasetAction'+ot.name] === undefined){
  1013. node.workflow_outputs.push(ot.name);
  1014. callout = $(node.element).find('.callout.'+ot.name);
  1015. callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
  1016. workflow.has_changes = true;
  1017. }
  1018. });
  1019. }
  1020. });
  1021. },
  1022. check_changes_in_active_form : function() {
  1023. // If active form has changed, save it
  1024. if (this.active_form_has_changes) {
  1025. this.has_changes = true;
  1026. // Submit form.
  1027. $("#right-content").find("form").submit();
  1028. this.active_form_has_changes = false;
  1029. }
  1030. },
  1031. reload_active_node : function() {
  1032. if (this.active_node){
  1033. var node = this.active_node;
  1034. this.clear_active_node();
  1035. this.activate_node(node);
  1036. }
  1037. },
  1038. clear_active_node : function() {
  1039. if ( this.active_node ) {
  1040. this.active_node.make_inactive();
  1041. this.active_node = null;
  1042. }
  1043. parent.show_form_for_tool( "<div>No node selected</div>" );
  1044. },
  1045. activate_node : function( node ) {
  1046. if ( this.active_node != node ) {
  1047. this.check_changes_in_active_form();
  1048. this.clear_active_node();
  1049. parent.show_form_for_tool( node.form_html + node.tooltip, node );
  1050. node.make_active();
  1051. this.active_node = node;
  1052. }
  1053. },
  1054. node_changed : function ( node ) {
  1055. this.has_changes = true;
  1056. if ( this.active_node == node ) {
  1057. // Reactive with new form_html
  1058. this.check_changes_in_active_form(); //Force changes to be saved even on new connection (previously dumped)
  1059. parent.show_form_for_tool( node.form_html + node.tooltip, node );
  1060. }
  1061. },
  1062. layout : function () {
  1063. this.check_changes_in_active_form();
  1064. this.has_changes = true;
  1065. // Prepare predecessor / successor tracking
  1066. var n_pred = {};
  1067. var successors = {};
  1068. // First pass to initialize arrays even for nodes with no connections
  1069. $.each( this.nodes, function( id, node ) {
  1070. if ( n_pred[id] === undefined ) { n_pred[id] = 0; }
  1071. if ( successors[id] === undefined ) { successors[id] = []; }
  1072. });
  1073. // Second pass to count predecessors and successors
  1074. $.each( this.nodes, function( id, node ) {
  1075. $.each( node.input_terminals, function ( j, t ) {
  1076. $.each( t.connectors, function ( k, c ) {
  1077. // A connection exists from `other` to `node`
  1078. var other = c.handle1.node;
  1079. // node gains a predecessor
  1080. n_pred[node.id] += 1;
  1081. // other gains a successor
  1082. successors[other.id].push( node.id );
  1083. });
  1084. });
  1085. });
  1086. // Assemble order, tracking levels
  1087. node_ids_by_level = [];
  1088. while ( true ) {
  1089. // Everything without a predecessor
  1090. level_parents = [];
  1091. for ( var pred_k in n_pred ) {
  1092. if ( n_pred[ pred_k ] == 0 ) {
  1093. level_parents.push( pred_k );
  1094. }
  1095. }
  1096. if ( level_parents.length == 0 ) {
  1097. break;
  1098. }
  1099. node_ids_by_level.push( level_parents );
  1100. // Remove the parents from this level, and decrement the number
  1101. // of predecessors for each successor
  1102. for ( var k in level_parents ) {
  1103. var v = level_parents[k];
  1104. delete n_pred[v];
  1105. for ( var sk in successors[v] ) {
  1106. n_pred[ successors[v][sk] ] -= 1;
  1107. }
  1108. }
  1109. }
  1110. if ( n_pred.length ) {
  1111. // ERROR: CYCLE! Currently we do nothing
  1112. return;
  1113. }
  1114. // Layout each level
  1115. var all_nodes = this.nodes;
  1116. var h_pad = 80; v_pad = 30;
  1117. var left = h_pad;
  1118. $.each( node_ids_by_level, function( i, ids ) {
  1119. // We keep nodes in the same order in a level to give the user
  1120. // some control over ordering
  1121. ids.sort( function( a, b ) {
  1122. return $(all_nodes[a].element).position().top - $(all_nodes[b].element).position().top;
  1123. });
  1124. // Position each node
  1125. var max_width = 0;
  1126. var top = v_pad;
  1127. $.each( ids, function( j, id ) {
  1128. var node = all_nodes[id];
  1129. var element = $(node.element);
  1130. $(element).css( { top: top, left: left } );
  1131. max_width = Math.max( max_width, $(element).width() );
  1132. top += $(element).height() + v_pad;
  1133. });
  1134. left += max_width + h_pad;
  1135. });
  1136. // Need to redraw all connectors
  1137. $.each( all_nodes, function( _, node ) { node.redraw(); } );
  1138. },
  1139. bounds_for_all_nodes: function() {
  1140. var xmin = Infinity, xmax = -Infinity,
  1141. ymin = Infinity, ymax = -Infinity,
  1142. p;
  1143. $.each( this.nodes, function( id, node ) {
  1144. e = $(node.element);
  1145. p = e.position();
  1146. xmin = Math.min( xmin, p.left );
  1147. xmax = Math.max( xmax, p.left + e.width() );
  1148. ymin = Math.min( ymin, p.top );
  1149. ymax = Math.max( ymax, p.top + e.width() );
  1150. });
  1151. return { xmin: xmin, xmax: xmax, ymin: ymin, ymax: ymax };
  1152. },
  1153. fit_canvas_to_nodes: function() {
  1154. // Span of all elements
  1155. var bounds = this.bounds_for_all_nodes();
  1156. var position = this.canvas_container.position();
  1157. var parent = this.canvas_container.parent();
  1158. // Determine amount we need to expand on top/left
  1159. var xmin_delta = fix_delta( bounds.xmin, 100 );
  1160. var ymin_delta = fix_delta( bounds.ymin, 100 );
  1161. // May need to expand farther to fill viewport
  1162. xmin_delta = Math.max( xmin_delta, position.left );
  1163. ymin_delta = Math.max( ymin_delta, position.top );
  1164. var left = position.left - xmin_delta;
  1165. var top = position.top - ymin_delta;
  1166. // Same for width/height
  1167. var width = round_up( bounds.xmax + 100, 100 ) + xmin_delta;
  1168. var height = round_up( bounds.ymax + 100, 100 ) + ymin_delta;
  1169. width = Math.max( width, - left + parent.width() );
  1170. height = Math.max( height, - top + parent.height() );
  1171. // Grow the canvas container
  1172. this.canvas_container.css( {
  1173. left: left,
  1174. top: top,
  1175. width: width,
  1176. height: height
  1177. });
  1178. // Move elements back if needed
  1179. this.canvas_container.children().each( function() {
  1180. var p = $(this).position();
  1181. $(this).css( "left", p.left + xmin_delta );
  1182. $(this).css( "top", p.top + ymin_delta );
  1183. });
  1184. }
  1185. });
  1186. function fix_delta( x, n ) {
  1187. if ( x < n|| x > 3*n ) {
  1188. new_pos = ( Math.ceil( ( ( x % n ) ) / n ) + 1 ) * n;
  1189. return ( - ( x - new_pos ) );
  1190. }
  1191. return 0;
  1192. }
  1193. function round_up( x, n ) {
  1194. return Math.ceil( x / n ) * n;
  1195. }
  1196. function prebuild_node( type, title_text, tool_id ) {
  1197. var f = $("<div class='toolForm toolFormInCanvas'></div>");
  1198. var node = new Node( { element: f } );
  1199. node.type = type;
  1200. if ( type == 'tool' ) {
  1201. node.tool_id = tool_id;
  1202. }
  1203. var title = $("<div class='toolFormTitle unselectable'>" + title_text + "</div>" );
  1204. f.append( title );
  1205. f.css( "left", $(window).scrollLeft() + 20 ); f.css( "top", $(window).scrollTop() + 20 );
  1206. var b = $("<div class='toolFormBody'></div>");
  1207. var tmp = "<div><img height='16' align='middle' src='" + galaxy_config.root + "static/images/loading_small_white_bg.gif'/> loading tool info...</div>";
  1208. b.append( tmp );
  1209. node.form_html = tmp;
  1210. f.append( b );
  1211. // Fix width to computed width
  1212. // Now add floats
  1213. var buttons = $("<div class='buttons' style='float: right;'></div>");
  1214. buttons.append( $("<div>").addClass("fa-icon-button fa fa-times").click( function( e ) {
  1215. node.destroy();
  1216. }));
  1217. // Place inside container
  1218. f.appendTo( "#canvas-container" );
  1219. // Position in container
  1220. var o = $("#canvas-container").position();
  1221. var p = $("#canvas-container").parent();
  1222. var width = f.width();
  1223. var height = f.height();
  1224. f.css( { left: ( - o.left ) + ( p.width() / 2 ) - ( width / 2 ), top: ( - o.top ) + ( p.height() / 2 ) - ( height / 2 ) } );
  1225. buttons.prependTo( title );
  1226. width += ( buttons.width() + 10 );
  1227. f.css( "width", width );
  1228. $(f).bind( "dragstart", function() {
  1229. workflow.activate_node( node );
  1230. }).bind( "dragend", function() {
  1231. workflow.node_changed( this );
  1232. workflow.fit_canvas_to_nodes();
  1233. canvas_manager.draw_overview();
  1234. }).bind( "dragclickonly", function() {
  1235. workflow.activate_node( node );
  1236. }).bind( "drag", function( e, d ) {
  1237. // Move
  1238. var po = $(this).offsetParent().offset(),
  1239. x = d.offsetX - po.left,
  1240. y = d.offsetY - po.top;
  1241. $(this).css( { left: x, top: y } );
  1242. // Redraw
  1243. $(this).find( ".terminal" ).each( function() {
  1244. this.terminal.redraw();
  1245. });
  1246. });
  1247. return node;
  1248. }
  1249. function add_node( type, title_text, tool_id ) {
  1250. // Abstraction for use by galaxy.workflow.js to hide
  1251. // some editor details from workflow code and reduce duplication
  1252. // between add_node_for_tool and add_node_for_module.
  1253. var node = prebuild_node( type, title_text, tool_id );
  1254. workflow.add_node( node );
  1255. workflow.fit_canvas_to_nodes();
  1256. canvas_manager.draw_overview();
  1257. workflow.activate_node( node );
  1258. return node;
  1259. }
  1260. var ext_to_type = null;
  1261. var type_to_type = null;
  1262. function issubtype( child, parent ) {
  1263. child = ext_to_type[child];
  1264. parent = ext_to_type[parent];
  1265. return ( type_to_type[child] ) && ( parent in type_to_type[child] );
  1266. }
  1267. function populate_datatype_info( data ) {
  1268. ext_to_type = data.ext_to_class_name;
  1269. type_to_type = data.class_to_classes;
  1270. }
  1271. //////////////
  1272. // START VIEWS
  1273. //////////////
  1274. var NodeView = Backbone.View.extend( {
  1275. initialize: function( options ){
  1276. this.node = options.node;
  1277. this.output_width = Math.max(150, this.$el.width());
  1278. this.tool_body = this.$el.find( ".toolFormBody" );
  1279. this.tool_body.find( "div" ).remove();
  1280. this.newInputsDiv().appendTo( this.tool_body );
  1281. this.terminalViews = {};
  1282. this.outputTerminlViews = {};
  1283. },
  1284. render : function() {
  1285. this.renderToolErrors();
  1286. this.$el.css( "width", Math.min(250, Math.max(this.$el.width(), this.output_width )));
  1287. },
  1288. renderToolErrors: function( ) {
  1289. if ( this.node.tool_errors ) {
  1290. this.$el.addClass( "tool-node-error" );
  1291. } else {
  1292. this.$el.removeClass( "tool-node-error" );
  1293. }
  1294. },
  1295. newInputsDiv: function() {
  1296. return $("<div class='inputs'></div>");
  1297. },
  1298. updateMaxWidth: function( newWidth ) {
  1299. this.output_width = Math.max( this.output_width, newWidth );
  1300. },
  1301. addRule: function() {
  1302. this.tool_body.append( $( "<div class='rule'></div>" ) );
  1303. },
  1304. addDataInput: function( input, body ) {
  1305. var skipResize = true;
  1306. if( ! body ) {
  1307. body = this.$( ".inputs" );
  1308. // initial addition to node - resize input to help calculate node
  1309. // width.
  1310. skipResize = false;
  1311. }
  1312. var terminalView = this.terminalViews[ input.name ];
  1313. var terminalViewClass = ( input.input_type == "dataset_collection" ) ? InputCollectionTerminalView : InputTerminalView;
  1314. if( terminalView && ! ( terminalView instanceof terminalViewClass ) ) {
  1315. terminalView.el.terminal.destroy();
  1316. terminalView = null;
  1317. }
  1318. if( ! terminalView ) {
  1319. terminalView = new terminalViewClass( {
  1320. node: this.node,
  1321. input: input
  1322. } );
  1323. } else {
  1324. var terminal = terminalView.el.terminal;
  1325. terminal.update( input );
  1326. terminal.destroyInvalidConnections();
  1327. }
  1328. this.terminalViews[ input.name ] = terminalView;
  1329. var terminalElement = terminalView.el;
  1330. var inputView = new DataInputView( {
  1331. terminalElement: terminalElement,
  1332. input: input,
  1333. nodeView: this,
  1334. skipResize: skipResize
  1335. } );
  1336. var ib = inputView.$el;
  1337. // Append to new body
  1338. body.append( ib.prepend( terminalView.terminalElements() ) );
  1339. return terminalView;
  1340. },
  1341. addDataOutput: function( output ) {
  1342. var terminalViewClass = ( output.collection_type ) ? OutputCollectionTerminalView : OutputTerminalView;
  1343. var terminalView = new terminalViewClass( {
  1344. node: this.node,
  1345. output: output
  1346. } );
  1347. var outputView = new DataOutputView( {
  1348. "output": output,
  1349. "terminalElement": terminalView.el,
  1350. "nodeView": this,
  1351. } );
  1352. this.tool_body.append( outputView.$el.append( terminalView.terminalElements() ) );
  1353. },
  1354. updateDataOutput: function( output ) {
  1355. var outputTerminal = this.node.output_terminals[ output.name ];
  1356. outputTerminal.update( output );
  1357. }
  1358. } );
  1359. var DataInputView = Backbone.View.extend( {
  1360. className: "form-row dataRow input-data-row",
  1361. initialize: function( options ){
  1362. this.input = options.input;
  1363. this.nodeView = options.nodeView;
  1364. this.terminalElement = options.terminalElement;
  1365. this.$el.attr( "name", this.input.name )
  1366. .html( this.input.label );
  1367. if( ! options.skipResize ) {
  1368. this.$el.css({ position:'absolute',
  1369. left: -1000,
  1370. top: -1000,
  1371. display:'none'});
  1372. $('body').append(this.el);
  1373. this.nodeView.updateMaxWidth( this.$el.outerWidth() );
  1374. this.$el.css({ position:'',
  1375. left:'',
  1376. top:'',
  1377. display:'' });
  1378. this.$el.remove();
  1379. }
  1380. },
  1381. } );
  1382. var OutputCalloutView = Backbone.View.extend( {
  1383. tagName: "div",
  1384. initialize: function( options ) {
  1385. this.label = options.label;
  1386. this.node = options.node;
  1387. this.output = options.output;
  1388. var view = this;
  1389. this.$el
  1390. .attr( "class", 'callout '+this.label )
  1391. .css( { display: 'none' } )
  1392. .append(
  1393. $("<div class='buttons'></div>").append(
  1394. $("<img/>").attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png').click( function() {
  1395. if ($.inArray(view.output.name, view.node.workflow_outputs) != -1){
  1396. view.node.workflow_outputs.splice($.inArray(view.output.name, view.node.workflow_outputs), 1);
  1397. view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png');
  1398. }else{
  1399. view.node.workflow_outputs.push(view.output.name);
  1400. view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
  1401. }
  1402. workflow.has_changes = true;
  1403. canvas_manager.draw_overview();
  1404. })))
  1405. .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." });
  1406. this.$el.css({
  1407. top: '50%',
  1408. margin:'-8px 0px 0px 0px',
  1409. right: 8
  1410. });
  1411. this.$el.show();
  1412. this.resetImage();
  1413. },
  1414. resetImage: function() {
  1415. if ($.inArray( this.output.name, this.node.workflow_outputs) === -1){
  1416. this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png');
  1417. } else{
  1418. this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
  1419. }
  1420. },
  1421. hoverImage: function() {
  1422. this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-yellow.png');
  1423. }
  1424. } );
  1425. var DataOutputView = Backbone.View.extend( {
  1426. className: "form-row dataRow",
  1427. initialize: function( options ) {
  1428. this.output = options.output;
  1429. this.terminalElement = options.terminalElement;
  1430. this.nodeView = options.nodeView;
  1431. var output = this.output;
  1432. var label = output.name;
  1433. var node = this.nodeView.node;
  1434. var isInput = output.extensions.indexOf( 'input' ) >= 0 || output.extensions.indexOf( 'input_collection' ) >= 0;
  1435. if ( ! isInput ) {
  1436. label = label + " (" + output.extensions.join(", ") + ")";
  1437. }
  1438. this.$el.html( label )
  1439. if (node.type == 'tool'){
  1440. var calloutView = new OutputCalloutView( {
  1441. "label": label,
  1442. "output": output,
  1443. "node": node,
  1444. });
  1445. this.$el.append( calloutView.el );
  1446. this.$el.hover( function() { calloutView.hoverImage() }, function() { calloutView.resetImage() } );
  1447. }
  1448. this.$el.css({ position:'absolute',
  1449. left: -1000,
  1450. top: -1000,
  1451. display:'none'});
  1452. $('body').append( this.el );
  1453. this.nodeView.updateMaxWidth( this.$el.outerWidth() + 17 );
  1454. this.$el.css({ position:'',
  1455. left:'',
  1456. top:'',
  1457. display:'' })
  1458. .detach();
  1459. }
  1460. } );
  1461. var TerminalView = Backbone.View.extend( {
  1462. setupMappingView: function( terminal ) {
  1463. var terminalMapping = new this.terminalMappingClass( { terminal: terminal } );
  1464. var terminalMappingView = new this.terminalMappingViewClass( { model: terminalMapping } );
  1465. terminalMappingView.render();
  1466. terminal.terminalMappingView = terminalMappingView;
  1467. this.terminalMappingView = terminalMappingView;
  1468. },
  1469. terminalElements: function() {
  1470. if( this.terminalMappingView ) {
  1471. return [ this.terminalMappingView.el, this.el ];
  1472. } else{
  1473. return [ this.el ];
  1474. }
  1475. }
  1476. } );
  1477. var BaseInputTerminalView = TerminalView.extend( {
  1478. className: "terminal input-terminal",
  1479. initialize: function( options ) {
  1480. var node = options.node;
  1481. var input = options.input;
  1482. var name = input.name;
  1483. var terminal = this.terminalForInput( input );
  1484. if( ! terminal.multiple ) {
  1485. this.setupMappingView( terminal );
  1486. }
  1487. this.el.terminal = terminal;
  1488. terminal.node = node;
  1489. terminal.name = name;
  1490. node.input_terminals[name] = terminal;
  1491. },
  1492. events: {
  1493. "dropinit": "onDropInit",
  1494. "dropstart": "onDropStart",
  1495. "dropend": "onDropEnd",
  1496. "drop": "onDrop",
  1497. "hover": "onHover",
  1498. },
  1499. onDropInit: function( e, d ) {
  1500. var terminal = this.el.terminal;
  1501. // Accept a dragable if it is an output terminal and has a
  1502. // compatible type
  1503. return $(d.drag).hasClass( "output-terminal" ) && terminal.canAccept( d.drag.terminal );
  1504. },
  1505. onDropStart: function( e, d ) {
  1506. if (d.proxy.terminal) {
  1507. d.proxy.terminal.connectors[0].inner_color = "#BBFFBB";
  1508. }
  1509. },
  1510. onDropEnd: function ( e, d ) {
  1511. if (d.proxy.terminal) {
  1512. d.proxy.terminal.connectors[0].inner_color = "#FFFFFF";
  1513. }
  1514. },
  1515. onDrop: function( e, d ) {
  1516. var terminal = this.el.terminal;
  1517. new Connector( d.drag.terminal, terminal ).redraw();
  1518. },
  1519. onHover: function() {
  1520. var element = this.el;
  1521. var terminal = element.terminal;
  1522. // If connected, create a popup to allow disconnection
  1523. if ( terminal.connectors.length > 0 ) {
  1524. // Create callout
  1525. var t = $("<div class='callout'></div>")
  1526. .css( { display: 'none' } )
  1527. .appendTo( "body" )
  1528. .append(
  1529. $("<div class='button'></div>").append(
  1530. $("<div/>").addClass("fa-icon-button fa fa-times").click( function() {
  1531. $.each( terminal.connectors, function( _, x ) {
  1532. if (x) {
  1533. x.destroy();
  1534. }
  1535. });
  1536. t.remove();
  1537. })))
  1538. .bind( "mouseleave", function() {
  1539. $(this).remove();
  1540. });
  1541. // Position it and show
  1542. t.css({
  1543. top: $(element).offset().top - 2,
  1544. left: $(element).offset().left - t.width(),
  1545. 'padding-right': $(element).width()
  1546. }).show();
  1547. }
  1548. },
  1549. } );
  1550. var InputTerminalView = BaseInputTerminalView.extend( {
  1551. terminalMappingClass: InputTerminalMapping,
  1552. terminalMappingViewClass: InputTerminalMappingView,
  1553. terminalForInput: function( input ) {
  1554. return new InputTerminal( { element: this.el, input: input } );
  1555. },
  1556. } );
  1557. var InputCollectionTerminalView = BaseInputTerminalView.extend( {
  1558. terminalMappingClass: InputCollectionTerminalMapping,
  1559. terminalMappingViewClass: InputCollectionTerminalMappingView,
  1560. terminalForInput: function( input ) {
  1561. return new InputCollectionTerminal( { element: this.el, input: input } );
  1562. },
  1563. } );
  1564. var BaseOutputTerminalView = TerminalView.extend( {
  1565. className: "terminal output-terminal",
  1566. initialize: function( options ) {
  1567. var node = options.node;
  1568. var output = options.output;
  1569. var name = output.name;
  1570. var terminal = this.terminalForOutput( output );
  1571. this.setupMappingView( terminal );
  1572. this.el.terminal = terminal;
  1573. terminal.node = node;
  1574. terminal.name = name;
  1575. node.output_terminals[name] = terminal;
  1576. },
  1577. events: {
  1578. "drag": "onDrag",
  1579. "dragstart": "onDragStart",
  1580. "dragend": "onDragEnd",
  1581. },
  1582. onDrag: function ( e, d ) {
  1583. var onmove = function() {
  1584. var po = $(d.proxy).offsetParent().offset(),
  1585. x = d.offsetX - po.left,
  1586. y = d.offsetY - po.top;
  1587. $(d.proxy).css( { left: x, top: y } );
  1588. d.proxy.terminal.redraw();
  1589. // FIXME: global
  1590. canvas_manager.update_viewport_overlay();
  1591. };
  1592. onmove();
  1593. $("#canvas-container").get(0).scroll_panel.test( e, onmove );
  1594. },
  1595. onDragStart: function( e, d ) {
  1596. $( d.available ).addClass( "input-terminal-active" );
  1597. // Save PJAs in the case of change datatype actions.
  1598. workflow.check_changes_in_active_form();
  1599. // Drag proxy div
  1600. var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' )
  1601. .appendTo( "#canvas-container" ).get(0);
  1602. // Terminal and connection to display noodle while dragging
  1603. h.terminal = new OutputTerminal( { element: h } );
  1604. var c = new Connector();
  1605. c.dragging = true;
  1606. c.connect( this.el.terminal, h.terminal );
  1607. return h;
  1608. },
  1609. onDragEnd: function ( e, d ) {
  1610. var connector = d.proxy.terminal.connectors[0];
  1611. // check_changes_in_active_form may change the state and cause a
  1612. // the connection to have already been destroyed. There must be better
  1613. // ways to handle this but the following check fixes some serious GUI
  1614. // bugs for now.
  1615. if(connector) {
  1616. connector.destroy();
  1617. }
  1618. $(d.proxy).remove();
  1619. $( d.available ).removeClass( "input-terminal-active" );
  1620. $("#canvas-container").get(0).scroll_panel.stop();
  1621. }
  1622. } );
  1623. var OutputTerminalView = BaseOutputTerminalView.extend( {
  1624. terminalMappingClass: OutputTerminalMapping,
  1625. terminalMappingViewClass: OutputTerminalMappingView,
  1626. terminalForOutput: function( output ) {
  1627. var type = output.extensions;
  1628. var terminal = new OutputTerminal( { element: this.el, datatypes: type } );
  1629. return terminal;
  1630. },
  1631. } );
  1632. var OutputCollectionTerminalView = BaseOutputTerminalView.extend( {
  1633. terminalMappingClass: OutputCollectionTerminalMapping,
  1634. terminalMappingViewClass: OutputCollectionTerminalMappingView,
  1635. terminalForOutput: function( output ) {
  1636. var collection_type = output.collection_type;
  1637. var terminal = new OutputCollectionTerminal( { element: this.el, collection_type: collection_type, datatypes: output.extensions } );
  1638. return terminal;
  1639. },
  1640. } );
  1641. ////////////
  1642. // END VIEWS
  1643. ////////////
  1644. // FIXME: merge scroll panel into CanvasManager, clean up hardcoded stuff.
  1645. function ScrollPanel( panel ) {
  1646. this.panel = panel;
  1647. }
  1648. $.extend( ScrollPanel.prototype, {
  1649. test: function( e, onmove ) {
  1650. clearTimeout( this.timeout );
  1651. var x = e.pageX,
  1652. y = e.pageY,
  1653. // Panel size and position
  1654. panel = $(this.panel),
  1655. panel_pos = panel.position(),
  1656. panel_w = panel.width(),
  1657. panel_h = panel.height(),
  1658. // Viewport size and offset
  1659. viewport = panel.parent(),
  1660. viewport_w = viewport.width(),
  1661. viewport_h = viewport.height(),
  1662. viewport_offset = viewport.offset(),
  1663. // Edges of viewport (in page coordinates)
  1664. min_x = viewport_offset.left,
  1665. min_y = viewport_offset.top,
  1666. max_x = min_x + viewport.width(),
  1667. max_y = min_y + viewport.height(),
  1668. // Legal panel range
  1669. p_min_x = - ( panel_w - ( viewport_w / 2 ) ),
  1670. p_min_y = - ( panel_h - ( viewport_h / 2 )),
  1671. p_max_x = ( viewport_w / 2 ),
  1672. p_max_y = ( viewport_h / 2 ),
  1673. // Did the panel move?
  1674. moved = false,
  1675. // Constants
  1676. close_dist = 5,
  1677. nudge = 23;
  1678. if ( x - close_dist < min_x ) {
  1679. if ( panel_pos.left < p_max_x ) {
  1680. var t = Math.min( nudge, p_max_x - panel_pos.left );
  1681. panel.css( "left", panel_pos.left + t );
  1682. moved = true;
  1683. }
  1684. } else if ( x + close_dist > max_x ) {
  1685. if ( panel_pos.left > p_min_x ) {
  1686. var t = Math.min( nudge, panel_pos.left - p_min_x );
  1687. panel.css( "left", panel_pos.left - t );
  1688. moved = true;
  1689. }
  1690. } else if ( y - close_dist < min_y ) {
  1691. if ( panel_pos.top < p_max_y ) {
  1692. var t = Math.min( nudge, p_max_y - panel_pos.top );
  1693. panel.css( "top", panel_pos.top + t );
  1694. moved = true;
  1695. }
  1696. } else if ( y + close_dist > max_y ) {
  1697. if ( panel_pos.top > p_min_y ) {
  1698. var t = Math.min( nudge, panel_pos.top - p_min_x );
  1699. panel.css( "top", ( panel_pos.top - t ) + "px" );
  1700. moved = true;
  1701. }
  1702. }
  1703. if ( moved ) {
  1704. // Keep moving even if mouse doesn't move
  1705. onmove();
  1706. var panel = this;
  1707. this.timeout = setTimeout( function() { panel.test( e, onmove ); }, 50 );
  1708. }
  1709. },
  1710. stop: function( e, ui ) {
  1711. clearTimeout( this.timeout );
  1712. }
  1713. });
  1714. function CanvasManager( canvas_viewport, overview ) {
  1715. this.cv = canvas_viewport;
  1716. this.cc = this.cv.find( "#canvas-container" );
  1717. this.oc = overview.find( "#overview-canvas" );
  1718. this.ov = overview.find( "#overview-viewport" );
  1719. // Make overview box draggable
  1720. this.init_drag();
  1721. }
  1722. $.extend( CanvasManager.prototype, {
  1723. init_drag : function () {
  1724. var self = this;
  1725. var move = function( x, y ) {
  1726. x = Math.min( x, self.cv.width() / 2 );
  1727. x = Math.max( x, - self.cc.width() + self.cv.width() / 2 );
  1728. y = Math.min( y, self.cv.height() / 2 );
  1729. y = Math.max( y, - self.cc.height() + self.cv.height() / 2 );
  1730. self.cc.css( {
  1731. left: x,
  1732. top: y
  1733. });
  1734. self.update_viewport_overlay();
  1735. };
  1736. // Dragging within canvas background
  1737. this.cc.each( function() {
  1738. this.scroll_panel = new ScrollPanel( this );
  1739. });
  1740. var x_adjust, y_adjust;
  1741. this.cv.bind( "dragstart", function() {
  1742. var o = $(this).offset();
  1743. var p = self.cc.position();
  1744. y_adjust = p.top - o.top;
  1745. x_adjust = p.left - o.left;
  1746. }).bind( "drag", function( e, d ) {
  1747. move( d.offsetX + x_adjust, d.offsetY + y_adjust );
  1748. }).bind( "dragend", function() {
  1749. workflow.fit_canvas_to_nodes();
  1750. self.draw_overview();
  1751. });
  1752. // Dragging for overview pane
  1753. this.ov.bind( "drag", function( e, d ) {
  1754. var in_w = self.cc.width(),
  1755. in_h = self.cc.height(),
  1756. o_w = self.oc.width(),
  1757. o_h = self.oc.height(),
  1758. p = $(this).offsetParent().offset(),
  1759. new_x_offset = d.offsetX - p.left,
  1760. new_y_offset = d.offsetY - p.top;
  1761. move( - ( new_x_offset / o_w * in_w ),
  1762. - ( new_y_offset / o_h * in_h ) );
  1763. }).bind( "dragend", function() {
  1764. workflow.fit_canvas_to_nodes();
  1765. self.draw_overview();
  1766. });
  1767. // Dragging for overview border (resize)
  1768. $("#overview-border").bind( "drag", function( e, d ) {
  1769. var op = $(this).offsetParent();
  1770. var opo = op.offset();
  1771. var new_size = Math.max( op.width() - ( d.offsetX - opo.left ),
  1772. op.height() - ( d.offsetY - opo.top ) );
  1773. $(this).css( {
  1774. width: new_size,
  1775. height: new_size
  1776. });
  1777. self.draw_overview();
  1778. });
  1779. /* Disable dragging for child element of the panel so that resizing can
  1780. only be done by dragging the borders */
  1781. $("#overview-border div").bind("drag", function() { });
  1782. },
  1783. update_viewport_overlay: function() {
  1784. var cc = this.cc,
  1785. cv = this.cv,
  1786. oc = this.oc,
  1787. ov = this.ov,
  1788. in_w = cc.width(),
  1789. in_h = cc.height(),
  1790. o_w = oc.width(),
  1791. o_h = oc.height(),
  1792. cc_pos = cc.position();
  1793. ov.css( {
  1794. left: - ( cc_pos.left / in_w * o_w ),
  1795. top: - ( cc_pos.top / in_h * o_h ),
  1796. // Subtract 2 to account for borders (maybe just change box sizing style instead?)
  1797. width: ( cv.width() / in_w * o_w ) - 2,
  1798. height: ( cv.height() / in_h * o_h ) - 2
  1799. });
  1800. },
  1801. draw_overview: function() {
  1802. var canvas_el = $("#overview-canvas"),
  1803. size = canvas_el.parent().parent().width(),
  1804. c = canvas_el.get(0).getContext("2d"),
  1805. in_w = $("#canvas-container").width(),
  1806. in_h = $("#canvas-container").height();
  1807. var o_h, shift_h, o_w, shift_w;
  1808. // Fit canvas into overview area
  1809. var cv_w = this.cv.width();
  1810. var cv_h = this.cv.height();
  1811. if ( in_w < cv_w && in_h < cv_h ) {
  1812. // Canvas is smaller than viewport
  1813. o_w = in_w / cv_w * size;
  1814. shift_w = ( size - o_w ) / 2;
  1815. o_h = in_h / cv_h * size;
  1816. shift_h = ( size - o_h ) / 2;
  1817. } else if ( in_w < in_h ) {
  1818. // Taller than wide
  1819. shift_h = 0;
  1820. o_h = size;
  1821. o_w = Math.ceil( o_h * in_w / in_h );
  1822. shift_w = ( size - o_w ) / 2;
  1823. } else {
  1824. // Wider than tall
  1825. o_w = size;
  1826. shift_w = 0;
  1827. o_h = Math.ceil( o_w * in_h / in_w );
  1828. shift_h = ( size - o_h ) / 2;
  1829. }
  1830. canvas_el.parent().css( {
  1831. left: shift_w,
  1832. top: shift_h,
  1833. width: o_w,
  1834. height: o_h
  1835. });
  1836. canvas_el.attr( "width", o_w );
  1837. canvas_el.attr( "height", o_h );
  1838. // Draw overview
  1839. $.each( workflow.nodes, function( id, node ) {
  1840. c.fillStyle = "#D2C099";
  1841. c.strokeStyle = "#D8B365";
  1842. c.lineWidth = 1;
  1843. var node_element = $(node.element),
  1844. position = node_element.position(),
  1845. x = position.left / in_w * o_w,
  1846. y = position.top / in_h * o_h,
  1847. w = node_element.width() / in_w * o_w,
  1848. h = node_element.height() / in_h * o_h;
  1849. if (node.tool_errors){
  1850. c.fillStyle = "#FFCCCC";
  1851. c.strokeStyle = "#AA6666";
  1852. } else if (node.workflow_outputs != undefined && node.workflow_outputs.length > 0){
  1853. c.fillStyle = "#E8A92D";
  1854. c.strokeStyle = "#E8A92D";
  1855. }
  1856. c.fillRect( x, y, w, h );
  1857. c.strokeRect( x, y, w, h );
  1858. });
  1859. this.update_viewport_overlay();
  1860. }
  1861. });