PageRenderTime 73ms CodeModel.GetById 31ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://bitbucket.org/afgane/galaxy-central
JavaScript | 2005 lines | 1735 code | 129 blank | 141 comment | 247 complexity | 88bb990d23624539023bf63f33809e10 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.label = data.label;
  753. this.uuid = data.uuid;
  754. this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : [];
  755. var node = this;
  756. var nodeView = new NodeView({
  757. el: this.element[ 0 ],
  758. node: node,
  759. });
  760. node.nodeView = nodeView;
  761. $.each( data.data_inputs, function( i, input ) {
  762. nodeView.addDataInput( input );
  763. });
  764. if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) {
  765. nodeView.addRule();
  766. }
  767. $.each( data.data_outputs, function( i, output ) {
  768. nodeView.addDataOutput( output );
  769. } );
  770. nodeView.render();
  771. workflow.node_changed( this, true);
  772. },
  773. update_field_data : function( data ) {
  774. var node = this;
  775. nodeView = node.nodeView;
  776. this.tool_state = data.tool_state;
  777. this.form_html = data.form_html;
  778. this.tool_errors = data.tool_errors;
  779. this.annotation = data['annotation'];
  780. if( "post_job_actions" in data ) {
  781. // Won't be present in response for data inputs
  782. var pja_in = $.parseJSON(data.post_job_actions);
  783. this.post_job_actions = pja_in ? pja_in : {};
  784. }
  785. node.nodeView.renderToolErrors();
  786. // Update input rows
  787. var old_body = nodeView.$( "div.inputs" );
  788. var new_body = nodeView.newInputsDiv();
  789. var newTerminalViews = {};
  790. _.each( data.data_inputs, function( input ) {
  791. var terminalView = node.nodeView.addDataInput( input, new_body );
  792. newTerminalViews[ input.name ] = terminalView;
  793. });
  794. // Cleanup any leftover terminals
  795. _.each( _.difference( _.values( nodeView.terminalViews ), _.values( newTerminalViews ) ), function( unusedView ) {
  796. unusedView.el.terminal.destroy();
  797. } );
  798. nodeView.terminalViews = newTerminalViews;
  799. // In general workflow editor assumes tool outputs don't change in # or
  800. // type (not really valid right?) but adding special logic here for
  801. // data collection input parameters that can have their collection
  802. // change.
  803. if( data.data_outputs.length == 1 && "collection_type" in data.data_outputs[ 0 ] ) {
  804. nodeView.updateDataOutput( data.data_outputs[ 0 ] );
  805. }
  806. old_body.replaceWith( new_body );
  807. // If active, reactivate with new form_html
  808. this.markChanged();
  809. this.redraw();
  810. },
  811. error : function ( text ) {
  812. var b = $(this.element).find( ".toolFormBody" );
  813. b.find( "div" ).remove();
  814. var tmp = "<div style='color: red; text-style: italic;'>" + text + "</div>";
  815. this.form_html = tmp;
  816. b.html( tmp );
  817. workflow.node_changed( this );
  818. },
  819. markChanged: function() {
  820. workflow.node_changed( this );
  821. }
  822. } );
  823. function Workflow( canvas_container ) {
  824. this.canvas_container = canvas_container;
  825. this.id_counter = 0;
  826. this.nodes = {};
  827. this.name = null;
  828. this.has_changes = false;
  829. this.active_form_has_changes = false;
  830. }
  831. $.extend( Workflow.prototype, {
  832. add_node : function( node ) {
  833. node.id = this.id_counter;
  834. node.element.attr( 'id', 'wf-node-step-' + node.id );
  835. this.id_counter++;
  836. this.nodes[ node.id ] = node;
  837. this.has_changes = true;
  838. node.workflow = this;
  839. },
  840. remove_node : function( node ) {
  841. if ( this.active_node == node ) {
  842. this.clear_active_node();
  843. }
  844. delete this.nodes[ node.id ] ;
  845. this.has_changes = true;
  846. },
  847. remove_all : function() {
  848. wf = this;
  849. $.each( this.nodes, function ( k, v ) {
  850. v.destroy();
  851. wf.remove_node( v );
  852. });
  853. },
  854. rectify_workflow_outputs : function() {
  855. // Find out if we're using workflow_outputs or not.
  856. var using_workflow_outputs = false;
  857. var has_existing_pjas = false;
  858. $.each( this.nodes, function ( k, node ) {
  859. if (node.workflow_outputs && node.workflow_outputs.length > 0){
  860. using_workflow_outputs = true;
  861. }
  862. $.each(node.post_job_actions, function(pja_id, pja){
  863. if (pja.action_type === "HideDatasetAction"){
  864. has_existing_pjas = true;
  865. }
  866. });
  867. });
  868. if (using_workflow_outputs !== false || has_existing_pjas !== false){
  869. // Using workflow outputs, or has existing pjas. Remove all PJAs and recreate based on outputs.
  870. $.each(this.nodes, function (k, node ){
  871. if (node.type === 'tool'){
  872. var node_changed = false;
  873. if (node.post_job_actions == null){
  874. node.post_job_actions = {};
  875. node_changed = true;
  876. }
  877. var pjas_to_rem = [];
  878. $.each(node.post_job_actions, function(pja_id, pja){
  879. if (pja.action_type == "HideDatasetAction"){
  880. pjas_to_rem.push(pja_id);
  881. }
  882. });
  883. if (pjas_to_rem.length > 0 ) {
  884. $.each(pjas_to_rem, function(i, pja_name){
  885. node_changed = true;
  886. delete node.post_job_actions[pja_name];
  887. });
  888. }
  889. if (using_workflow_outputs){
  890. $.each(node.output_terminals, function(ot_id, ot){
  891. var create_pja = true;
  892. $.each(node.workflow_outputs, function(i, wo_name){
  893. if (ot.name === wo_name){
  894. create_pja = false;
  895. }
  896. });
  897. if (create_pja === true){
  898. node_changed = true;
  899. var pja = {
  900. action_type : "HideDatasetAction",
  901. output_name : ot.name,
  902. action_arguments : {}
  903. }
  904. node.post_job_actions['HideDatasetAction'+ot.name] = null;
  905. node.post_job_actions['HideDatasetAction'+ot.name] = pja;
  906. }
  907. });
  908. }
  909. // lastly, if this is the active node, and we made changes, reload the display at right.
  910. if (workflow.active_node == node && node_changed === true) {
  911. workflow.reload_active_node();
  912. }
  913. }
  914. });
  915. }
  916. },
  917. to_simple : function () {
  918. var nodes = {};
  919. $.each( this.nodes, function ( i, node ) {
  920. var input_connections = {};
  921. $.each( node.input_terminals, function ( k, t ) {
  922. input_connections[ t.name ] = null;
  923. // There should only be 0 or 1 connectors, so this is
  924. // really a sneaky if statement
  925. var cons = []
  926. $.each( t.connectors, function ( i, c ) {
  927. cons[i] = { id: c.handle1.node.id, output_name: c.handle1.name };
  928. input_connections[ t.name ] = cons;
  929. });
  930. });
  931. var post_job_actions = {};
  932. if (node.post_job_actions){
  933. $.each( node.post_job_actions, function ( i, act ) {
  934. var pja = {
  935. action_type : act.action_type,
  936. output_name : act.output_name,
  937. action_arguments : act.action_arguments
  938. }
  939. post_job_actions[ act.action_type + act.output_name ] = null;
  940. post_job_actions[ act.action_type + act.output_name ] = pja;
  941. });
  942. }
  943. if (!node.workflow_outputs){
  944. node.workflow_outputs = [];
  945. // Just in case.
  946. }
  947. var node_data = {
  948. id : node.id,
  949. type : node.type,
  950. tool_id : node.tool_id,
  951. tool_state : node.tool_state,
  952. tool_errors : node.tool_errors,
  953. input_connections : input_connections,
  954. position : $(node.element).position(),
  955. annotation: node.annotation,
  956. post_job_actions: node.post_job_actions,
  957. uuid: node.uuid,
  958. label: node.label,
  959. workflow_outputs: node.workflow_outputs
  960. };
  961. nodes[ node.id ] = node_data;
  962. });
  963. return { steps: nodes };
  964. },
  965. from_simple : function ( data ) {
  966. wf = this;
  967. var max_id = 0;
  968. wf.name = data.name;
  969. // First pass, nodes
  970. var using_workflow_outputs = false;
  971. $.each( data.steps, function( id, step ) {
  972. var node = prebuild_node( step.type, step.name, step.tool_id );
  973. node.init_field_data( step );
  974. if ( step.position ) {
  975. node.element.css( { top: step.position.top, left: step.position.left } );
  976. }
  977. node.id = step.id;
  978. wf.nodes[ node.id ] = node;
  979. max_id = Math.max( max_id, parseInt( id ) );
  980. // For older workflows, it's possible to have HideDataset PJAs, but not WorkflowOutputs.
  981. // Check for either, and then add outputs in the next pass.
  982. if (!using_workflow_outputs && node.type === 'tool'){
  983. if (node.workflow_outputs.length > 0){
  984. using_workflow_outputs = true;
  985. }
  986. else{
  987. $.each(node.post_job_actions, function(pja_id, pja){
  988. if (pja.action_type === "HideDatasetAction"){
  989. using_workflow_outputs = true;
  990. }
  991. });
  992. }
  993. }
  994. });
  995. wf.id_counter = max_id + 1;
  996. // Second pass, connections
  997. $.each( data.steps, function( id, step ) {
  998. var node = wf.nodes[id];
  999. $.each( step.input_connections, function( k, v ) {
  1000. if ( v ) {
  1001. if ( ! $.isArray( v ) ) {
  1002. v = [ v ];
  1003. }
  1004. $.each( v, function( l, x ) {
  1005. var other_node = wf.nodes[ x.id ];
  1006. var c = new Connector();
  1007. c.connect( other_node.output_terminals[ x.output_name ],
  1008. node.input_terminals[ k ] );
  1009. c.redraw();
  1010. });
  1011. }
  1012. });
  1013. if(using_workflow_outputs && node.type === 'tool'){
  1014. // Ensure that every output terminal has a WorkflowOutput or HideDatasetAction.
  1015. $.each(node.output_terminals, function(ot_id, ot){
  1016. if(node.post_job_actions['HideDatasetAction'+ot.name] === undefined){
  1017. node.workflow_outputs.push(ot.name);
  1018. callout = $(node.element).find('.callout.'+ot.name);
  1019. callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
  1020. workflow.has_changes = true;
  1021. }
  1022. });
  1023. }
  1024. });
  1025. },
  1026. check_changes_in_active_form : function() {
  1027. // If active form has changed, save it
  1028. if (this.active_form_has_changes) {
  1029. this.has_changes = true;
  1030. // Submit form.
  1031. $("#right-content").find("form").submit();
  1032. this.active_form_has_changes = false;
  1033. }
  1034. },
  1035. reload_active_node : function() {
  1036. if (this.active_node){
  1037. var node = this.active_node;
  1038. this.clear_active_node();
  1039. this.activate_node(node);
  1040. }
  1041. },
  1042. clear_active_node : function() {
  1043. if ( this.active_node ) {
  1044. this.active_node.make_inactive();
  1045. this.active_node = null;
  1046. }
  1047. parent.show_form_for_tool( "<div>No node selected</div>" );
  1048. },
  1049. activate_node : function( node ) {
  1050. if ( this.active_node != node ) {
  1051. this.check_changes_in_active_form();
  1052. this.clear_active_node();
  1053. if (parent.__NEWTOOLFORM__) {
  1054. parent.show_form_for_tool( node.form_html, node );
  1055. } else {
  1056. parent.show_form_for_tool( node.form_html + node.tooltip, node );
  1057. }
  1058. node.make_active();
  1059. this.active_node = node;
  1060. }
  1061. },
  1062. node_changed : function ( node, force ) {
  1063. this.has_changes = true;
  1064. if ( this.active_node == node && (!parent.__NEWTOOLFORM__ || force)) {
  1065. // Reactive with new form_html
  1066. this.check_changes_in_active_form(); //Force changes to be saved even on new connection (previously dumped)
  1067. if (parent.__NEWTOOLFORM__) {
  1068. parent.show_form_for_tool( node.form_html, node );
  1069. } else {
  1070. parent.show_form_for_tool( node.form_html + node.tooltip, node );
  1071. }
  1072. }
  1073. },
  1074. layout : function () {
  1075. this.check_changes_in_active_form();
  1076. this.has_changes = true;
  1077. // Prepare predecessor / successor tracking
  1078. var n_pred = {};
  1079. var successors = {};
  1080. // First pass to initialize arrays even for nodes with no connections
  1081. $.each( this.nodes, function( id, node ) {
  1082. if ( n_pred[id] === undefined ) { n_pred[id] = 0; }
  1083. if ( successors[id] === undefined ) { successors[id] = []; }
  1084. });
  1085. // Second pass to count predecessors and successors
  1086. $.each( this.nodes, function( id, node ) {
  1087. $.each( node.input_terminals, function ( j, t ) {
  1088. $.each( t.connectors, function ( k, c ) {
  1089. // A connection exists from `other` to `node`
  1090. var other = c.handle1.node;
  1091. // node gains a predecessor
  1092. n_pred[node.id] += 1;
  1093. // other gains a successor
  1094. successors[other.id].push( node.id );
  1095. });
  1096. });
  1097. });
  1098. // Assemble order, tracking levels
  1099. node_ids_by_level = [];
  1100. while ( true ) {
  1101. // Everything without a predecessor
  1102. level_parents = [];
  1103. for ( var pred_k in n_pred ) {
  1104. if ( n_pred[ pred_k ] == 0 ) {
  1105. level_parents.push( pred_k );
  1106. }
  1107. }
  1108. if ( level_parents.length == 0 ) {
  1109. break;
  1110. }
  1111. node_ids_by_level.push( level_parents );
  1112. // Remove the parents from this level, and decrement the number
  1113. // of predecessors for each successor
  1114. for ( var k in level_parents ) {
  1115. var v = level_parents[k];
  1116. delete n_pred[v];
  1117. for ( var sk in successors[v] ) {
  1118. n_pred[ successors[v][sk] ] -= 1;
  1119. }
  1120. }
  1121. }
  1122. if ( n_pred.length ) {
  1123. // ERROR: CYCLE! Currently we do nothing
  1124. return;
  1125. }
  1126. // Layout each level
  1127. var all_nodes = this.nodes;
  1128. var h_pad = 80; v_pad = 30;
  1129. var left = h_pad;
  1130. $.each( node_ids_by_level, function( i, ids ) {
  1131. // We keep nodes in the same order in a level to give the user
  1132. // some control over ordering
  1133. ids.sort( function( a, b ) {
  1134. return $(all_nodes[a].element).position().top - $(all_nodes[b].element).position().top;
  1135. });
  1136. // Position each node
  1137. var max_width = 0;
  1138. var top = v_pad;
  1139. $.each( ids, function( j, id ) {
  1140. var node = all_nodes[id];
  1141. var element = $(node.element);
  1142. $(element).css( { top: top, left: left } );
  1143. max_width = Math.max( max_width, $(element).width() );
  1144. top += $(element).height() + v_pad;
  1145. });
  1146. left += max_width + h_pad;
  1147. });
  1148. // Need to redraw all connectors
  1149. $.each( all_nodes, function( _, node ) { node.redraw(); } );
  1150. },
  1151. bounds_for_all_nodes: function() {
  1152. var xmin = Infinity, xmax = -Infinity,
  1153. ymin = Infinity, ymax = -Infinity,
  1154. p;
  1155. $.each( this.nodes, function( id, node ) {
  1156. e = $(node.element);
  1157. p = e.position();
  1158. xmin = Math.min( xmin, p.left );
  1159. xmax = Math.max( xmax, p.left + e.width() );
  1160. ymin = Math.min( ymin, p.top );
  1161. ymax = Math.max( ymax, p.top + e.width() );
  1162. });
  1163. return { xmin: xmin, xmax: xmax, ymin: ymin, ymax: ymax };
  1164. },
  1165. fit_canvas_to_nodes: function() {
  1166. // Span of all elements
  1167. var bounds = this.bounds_for_all_nodes();
  1168. var position = this.canvas_container.position();
  1169. var parent = this.canvas_container.parent();
  1170. // Determine amount we need to expand on top/left
  1171. var xmin_delta = fix_delta( bounds.xmin, 100 );
  1172. var ymin_delta = fix_delta( bounds.ymin, 100 );
  1173. // May need to expand farther to fill viewport
  1174. xmin_delta = Math.max( xmin_delta, position.left );
  1175. ymin_delta = Math.max( ymin_delta, position.top );
  1176. var left = position.left - xmin_delta;
  1177. var top = position.top - ymin_delta;
  1178. // Same for width/height
  1179. var width = round_up( bounds.xmax + 100, 100 ) + xmin_delta;
  1180. var height = round_up( bounds.ymax + 100, 100 ) + ymin_delta;
  1181. width = Math.max( width, - left + parent.width() );
  1182. height = Math.max( height, - top + parent.height() );
  1183. // Grow the canvas container
  1184. this.canvas_container.css( {
  1185. left: left,
  1186. top: top,
  1187. width: width,
  1188. height: height
  1189. });
  1190. // Move elements back if needed
  1191. this.canvas_container.children().each( function() {
  1192. var p = $(this).position();
  1193. $(this).css( "left", p.left + xmin_delta );
  1194. $(this).css( "top", p.top + ymin_delta );
  1195. });
  1196. }
  1197. });
  1198. function fix_delta( x, n ) {
  1199. if ( x < n|| x > 3*n ) {
  1200. new_pos = ( Math.ceil( ( ( x % n ) ) / n ) + 1 ) * n;
  1201. return ( - ( x - new_pos ) );
  1202. }
  1203. return 0;
  1204. }
  1205. function round_up( x, n ) {
  1206. return Math.ceil( x / n ) * n;
  1207. }
  1208. function prebuild_node( type, title_text, tool_id ) {
  1209. var f = $("<div class='toolForm toolFormInCanvas'></div>");
  1210. var node = new Node( { element: f } );
  1211. node.type = type;
  1212. if ( type == 'tool' ) {
  1213. node.tool_id = tool_id;
  1214. }
  1215. var title = $("<div class='toolFormTitle unselectable'>" + title_text + "</div>" );
  1216. f.append( title );
  1217. f.css( "left", $(window).scrollLeft() + 20 ); f.css( "top", $(window).scrollTop() + 20 );
  1218. var b = $("<div class='toolFormBody'></div>");
  1219. var tmp = "<div><img height='16' align='middle' src='" + galaxy_config.root + "static/images/loading_small_white_bg.gif'/> loading tool info...</div>";
  1220. b.append( tmp );
  1221. node.form_html = tmp;
  1222. f.append( b );
  1223. // Fix width to computed width
  1224. // Now add floats
  1225. var buttons = $("<div class='buttons' style='float: right;'></div>");
  1226. buttons.append( $("<div>").addClass("fa-icon-button fa fa-times").click( function( e ) {
  1227. node.destroy();
  1228. }));
  1229. // Place inside container
  1230. f.appendTo( "#canvas-container" );
  1231. // Position in container
  1232. var o = $("#canvas-container").position();
  1233. var p = $("#canvas-container").parent();
  1234. var width = f.width();
  1235. var height = f.height();
  1236. f.css( { left: ( - o.left ) + ( p.width() / 2 ) - ( width / 2 ), top: ( - o.top ) + ( p.height() / 2 ) - ( height / 2 ) } );
  1237. buttons.prependTo( title );
  1238. width += ( buttons.width() + 10 );
  1239. f.css( "width", width );
  1240. $(f).bind( "dragstart", function() {
  1241. workflow.activate_node( node );
  1242. }).bind( "dragend", function() {
  1243. workflow.node_changed( this );
  1244. workflow.fit_canvas_to_nodes();
  1245. canvas_manager.draw_overview();
  1246. }).bind( "dragclickonly", function() {
  1247. workflow.activate_node( node );
  1248. }).bind( "drag", function( e, d ) {
  1249. // Move
  1250. var po = $(this).offsetParent().offset(),
  1251. x = d.offsetX - po.left,
  1252. y = d.offsetY - po.top;
  1253. $(this).css( { left: x, top: y } );
  1254. // Redraw
  1255. $(this).find( ".terminal" ).each( function() {
  1256. this.terminal.redraw();
  1257. });
  1258. });
  1259. return node;
  1260. }
  1261. function add_node( type, title_text, tool_id ) {
  1262. // Abstraction for use by galaxy.workflow.js to hide
  1263. // some editor details from workflow code and reduce duplication
  1264. // between add_node_for_tool and add_node_for_module.
  1265. var node = prebuild_node( type, title_text, tool_id );
  1266. workflow.add_node( node );
  1267. workflow.fit_canvas_to_nodes();
  1268. canvas_manager.draw_overview();
  1269. workflow.activate_node( node );
  1270. return node;
  1271. }
  1272. var ext_to_type = null;
  1273. var type_to_type = null;
  1274. function issubtype( child, parent ) {
  1275. child = ext_to_type[child];
  1276. parent = ext_to_type[parent];
  1277. return ( type_to_type[child] ) && ( parent in type_to_type[child] );
  1278. }
  1279. function populate_datatype_info( data ) {
  1280. ext_to_type = data.ext_to_class_name;
  1281. type_to_type = data.class_to_classes;
  1282. }
  1283. //////////////
  1284. // START VIEWS
  1285. //////////////
  1286. var NodeView = Backbone.View.extend( {
  1287. initialize: function( options ){
  1288. this.node = options.node;
  1289. this.output_width = Math.max(150, this.$el.width());
  1290. this.tool_body = this.$el.find( ".toolFormBody" );
  1291. this.tool_body.find( "div" ).remove();
  1292. this.newInputsDiv().appendTo( this.tool_body );
  1293. this.terminalViews = {};
  1294. this.outputTerminlViews = {};
  1295. },
  1296. render : function() {
  1297. this.renderToolErrors();
  1298. this.$el.css( "width", Math.min(250, Math.max(this.$el.width(), this.output_width )));
  1299. },
  1300. renderToolErrors: function( ) {
  1301. if ( this.node.tool_errors ) {
  1302. this.$el.addClass( "tool-node-error" );
  1303. } else {
  1304. this.$el.removeClass( "tool-node-error" );
  1305. }
  1306. },
  1307. newInputsDiv: function() {
  1308. return $("<div class='inputs'></div>");
  1309. },
  1310. updateMaxWidth: function( newWidth ) {
  1311. this.output_width = Math.max( this.output_width, newWidth );
  1312. },
  1313. addRule: function() {
  1314. this.tool_body.append( $( "<div class='rule'></div>" ) );
  1315. },
  1316. addDataInput: function( input, body ) {
  1317. var skipResize = true;
  1318. if( ! body ) {
  1319. body = this.$( ".inputs" );
  1320. // initial addition to node - resize input to help calculate node
  1321. // width.
  1322. skipResize = false;
  1323. }
  1324. var terminalView = this.terminalViews[ input.name ];
  1325. var terminalViewClass = ( input.input_type == "dataset_collection" ) ? InputCollectionTerminalView : InputTerminalView;
  1326. if( terminalView && ! ( terminalView instanceof terminalViewClass ) ) {
  1327. terminalView.el.terminal.destroy();
  1328. terminalView = null;
  1329. }
  1330. if( ! terminalView ) {
  1331. terminalView = new terminalViewClass( {
  1332. node: this.node,
  1333. input: input
  1334. } );
  1335. } else {
  1336. var terminal = terminalView.el.terminal;
  1337. terminal.update( input );
  1338. terminal.destroyInvalidConnections();
  1339. }
  1340. this.terminalViews[ input.name ] = terminalView;
  1341. var terminalElement = terminalView.el;
  1342. var inputView = new DataInputView( {
  1343. terminalElement: terminalElement,
  1344. input: input,
  1345. nodeView: this,
  1346. skipResize: skipResize
  1347. } );
  1348. var ib = inputView.$el;
  1349. // Append to new body
  1350. body.append( ib.prepend( terminalView.terminalElements() ) );
  1351. return terminalView;
  1352. },
  1353. addDataOutput: function( output ) {
  1354. var terminalViewClass = ( output.collection_type ) ? OutputCollectionTerminalView : OutputTerminalView;
  1355. var terminalView = new terminalViewClass( {
  1356. node: this.node,
  1357. output: output
  1358. } );
  1359. var outputView = new DataOutputView( {
  1360. "output": output,
  1361. "terminalElement": terminalView.el,
  1362. "nodeView": this,
  1363. } );
  1364. this.tool_body.append( outputView.$el.append( terminalView.terminalElements() ) );
  1365. },
  1366. updateDataOutput: function( output ) {
  1367. var outputTerminal = this.node.output_terminals[ output.name ];
  1368. outputTerminal.update( output );
  1369. }
  1370. } );
  1371. var DataInputView = Backbone.View.extend( {
  1372. className: "form-row dataRow input-data-row",
  1373. initialize: function( options ){
  1374. this.input = options.input;
  1375. this.nodeView = options.nodeView;
  1376. this.terminalElement = options.terminalElement;
  1377. this.$el.attr( "name", this.input.name )
  1378. .html( this.input.label );
  1379. if( ! options.skipResize ) {
  1380. this.$el.css({ position:'absolute',
  1381. left: -1000,
  1382. top: -1000,
  1383. display:'none'});
  1384. $('body').append(this.el);
  1385. this.nodeView.updateMaxWidth( this.$el.outerWidth() );
  1386. this.$el.css({ position:'',
  1387. left:'',
  1388. top:'',
  1389. display:'' });
  1390. this.$el.remove();
  1391. }
  1392. },
  1393. } );
  1394. var OutputCalloutView = Backbone.View.extend( {
  1395. tagName: "div",
  1396. initialize: function( options ) {
  1397. this.label = options.label;
  1398. this.node = options.node;
  1399. this.output = options.output;
  1400. var view = this;
  1401. this.$el
  1402. .attr( "class", 'callout '+this.label )
  1403. .css( { display: 'none' } )
  1404. .append(
  1405. $("<div class='buttons'></div>").append(
  1406. $("<img/>").attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png').click( function() {
  1407. if ($.inArray(view.output.name, view.node.workflow_outputs) != -1){
  1408. view.node.workflow_outputs.splice($.inArray(view.output.name, view.node.workflow_outputs), 1);
  1409. view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png');
  1410. }else{
  1411. view.node.workflow_outputs.push(view.output.name);
  1412. view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
  1413. }
  1414. workflow.has_changes = true;
  1415. canvas_manager.draw_overview();
  1416. })))
  1417. .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." });
  1418. this.$el.css({
  1419. top: '50%',
  1420. margin:'-8px 0px 0px 0px',
  1421. right: 8
  1422. });
  1423. this.$el.show();
  1424. this.resetImage();
  1425. },
  1426. resetImage: function() {
  1427. if ($.inArray( this.output.name, this.node.workflow_outputs) === -1){
  1428. this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png');
  1429. } else{
  1430. this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
  1431. }
  1432. },
  1433. hoverImage: function() {
  1434. this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-yellow.png');
  1435. }
  1436. } );
  1437. var DataOutputView = Backbone.View.extend( {
  1438. className: "form-row dataRow",
  1439. initialize: function( options ) {
  1440. this.output = options.output;
  1441. this.terminalElement = options.terminalElement;
  1442. this.nodeView = options.nodeView;
  1443. var output = this.output;
  1444. var label = output.name;
  1445. var node = this.nodeView.node;
  1446. var isInput = output.extensions.indexOf( 'input' ) >= 0 || output.extensions.indexOf( 'input_collection' ) >= 0;
  1447. if ( ! isInput ) {
  1448. label = label + " (" + output.extensions.join(", ") + ")";
  1449. }
  1450. this.$el.html( label )
  1451. if (node.type == 'tool'){
  1452. var calloutView = new OutputCalloutView( {
  1453. "label": label,
  1454. "output": output,
  1455. "node": node,
  1456. });
  1457. this.$el.append( calloutView.el );
  1458. this.$el.hover( function() { calloutView.hoverImage() }, function() { calloutView.resetImage() } );
  1459. }
  1460. this.$el.css({ position:'absolute',
  1461. left: -1000,
  1462. top: -1000,
  1463. display:'none'});
  1464. $('body').append( this.el );
  1465. this.nodeView.updateMaxWidth( this.$el.outerWidth() + 17 );
  1466. this.$el.css({ position:'',
  1467. left:'',
  1468. top:'',
  1469. display:'' })
  1470. .detach();
  1471. }
  1472. } );
  1473. var TerminalView = Backbone.View.extend( {
  1474. setupMappingView: function( terminal ) {
  1475. var terminalMapping = new this.terminalMappingClass( { terminal: terminal } );
  1476. var terminalMappingView = new this.terminalMappingViewClass( { model: terminalMapping } );
  1477. terminalMappingView.render();
  1478. terminal.terminalMappingView = terminalMappingView;
  1479. this.terminalMappingView = terminalMappingView;
  1480. },
  1481. terminalElements: function() {
  1482. if( this.terminalMappingView ) {
  1483. return [ this.terminalMappingView.el, this.el ];
  1484. } else{
  1485. return [ this.el ];
  1486. }
  1487. }
  1488. } );
  1489. var BaseInputTerminalView = TerminalView.extend( {
  1490. className: "terminal input-terminal",
  1491. initialize: function( options ) {
  1492. var node = options.node;
  1493. var input = options.input;
  1494. var name = input.name;
  1495. var terminal = this.terminalForInput( input );
  1496. if( ! terminal.multiple ) {
  1497. this.setupMappingView( terminal );
  1498. }
  1499. this.el.terminal = terminal;
  1500. terminal.node = node;
  1501. terminal.name = name;
  1502. node.input_terminals[name] = terminal;
  1503. },
  1504. events: {
  1505. "dropinit": "onDropInit",
  1506. "dropstart": "onDropStart",
  1507. "dropend": "onDropEnd",
  1508. "drop": "onDrop",
  1509. "hover": "onHover",
  1510. },
  1511. onDropInit: function( e, d ) {
  1512. var terminal = this.el.terminal;
  1513. // Accept a dragable if it is an output terminal and has a
  1514. // compatible type
  1515. return $(d.drag).hasClass( "output-terminal" ) && terminal.canAccept( d.drag.terminal );
  1516. },
  1517. onDropStart: function( e, d ) {
  1518. if (d.proxy.terminal) {
  1519. d.proxy.terminal.connectors[0].inner_color = "#BBFFBB";
  1520. }
  1521. },
  1522. onDropEnd: function ( e, d ) {
  1523. if (d.proxy.terminal) {
  1524. d.proxy.terminal.connectors[0].inner_color = "#FFFFFF";
  1525. }
  1526. },
  1527. onDrop: function( e, d ) {
  1528. var terminal = this.el.terminal;
  1529. new Connector( d.drag.terminal, terminal ).redraw();
  1530. },
  1531. onHover: function() {
  1532. var element = this.el;
  1533. var terminal = element.terminal;
  1534. // If connected, create a popup to allow disconnection
  1535. if ( terminal.connectors.length > 0 ) {
  1536. // Create callout
  1537. var t = $("<div class='callout'></div>")
  1538. .css( { display: 'none' } )
  1539. .appendTo( "body" )
  1540. .append(
  1541. $("<div class='button'></div>").append(
  1542. $("<div/>").addClass("fa-icon-button fa fa-times").click( function() {
  1543. $.each( terminal.connectors, function( _, x ) {
  1544. if (x) {
  1545. x.destroy();
  1546. }
  1547. });
  1548. t.remove();
  1549. })))
  1550. .bind( "mouseleave", function() {
  1551. $(this).remove();
  1552. });
  1553. // Position it and show
  1554. t.css({
  1555. top: $(element).offset().top - 2,
  1556. left: $(element).offset().left - t.width(),
  1557. 'padding-right': $(element).width()
  1558. }).show();
  1559. }
  1560. },
  1561. } );
  1562. var InputTerminalView = BaseInputTerminalView.extend( {
  1563. terminalMappingClass: InputTerminalMapping,
  1564. terminalMappingViewClass: InputTerminalMappingView,
  1565. terminalForInput: function( input ) {
  1566. return new InputTerminal( { element: this.el, input: input } );
  1567. },
  1568. } );
  1569. var InputCollectionTerminalView = BaseInputTerminalView.extend( {
  1570. terminalMappingClass: InputCollectionTerminalMapping,
  1571. terminalMappingViewClass: InputCollectionTerminalMappingView,
  1572. terminalForInput: function( input ) {
  1573. return new InputCollectionTerminal( { element: this.el, input: input } );
  1574. },
  1575. } );
  1576. var BaseOutputTerminalView = TerminalView.extend( {
  1577. className: "terminal output-terminal",
  1578. initialize: function( options ) {
  1579. var node = options.node;
  1580. var output = options.output;
  1581. var name = output.name;
  1582. var terminal = this.terminalForOutput( output );
  1583. this.setupMappingView( terminal );
  1584. this.el.terminal = terminal;
  1585. terminal.node = node;
  1586. terminal.name = name;
  1587. node.output_terminals[name] = terminal;
  1588. },
  1589. events: {
  1590. "drag": "onDrag",
  1591. "dragstart": "onDragStart",
  1592. "dragend": "onDragEnd",
  1593. },
  1594. onDrag: function ( e, d ) {
  1595. var onmove = function() {
  1596. var po = $(d.proxy).offsetParent().offset(),
  1597. x = d.offsetX - po.left,
  1598. y = d.offsetY - po.top;
  1599. $(d.proxy).css( { left: x, top: y } );
  1600. d.proxy.terminal.redraw();
  1601. // FIXME: global
  1602. canvas_manager.update_viewport_overlay();
  1603. };
  1604. onmove();
  1605. $("#canvas-container").get(0).scroll_panel.test( e, onmove );
  1606. },
  1607. onDragStart: function( e, d ) {
  1608. $( d.available ).addClass( "input-terminal-active" );
  1609. // Save PJAs in the case of change datatype actions.
  1610. workflow.check_changes_in_active_form();
  1611. // Drag proxy div
  1612. var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' )
  1613. .appendTo( "#canvas-container" ).get(0);
  1614. // Terminal and connection to display noodle while dragging
  1615. h.terminal = new OutputTerminal( { element: h } );
  1616. var c = new Connector();
  1617. c.dragging = true;
  1618. c.connect( this.el.terminal, h.terminal );
  1619. return h;
  1620. },
  1621. onDragEnd: function ( e, d ) {
  1622. var connector = d.proxy.terminal.connectors[0];
  1623. // check_changes_in_active_form may change the state and cause a
  1624. // the connection to have already been destroyed. There must be better
  1625. // ways to handle this but the following check fixes some serious GUI
  1626. // bugs for now.
  1627. if(connector) {
  1628. connector.destroy();
  1629. }
  1630. $(d.proxy).remove();
  1631. $( d.available ).removeClass( "input-terminal-active" );
  1632. $("#canvas-container").get(0).scroll_panel.stop();
  1633. }
  1634. } );
  1635. var OutputTerminalView = BaseOutputTerminalView.extend( {
  1636. terminalMappingClass: OutputTerminalMapping,
  1637. terminalMappingViewClass: OutputTerminalMappingView,
  1638. terminalForOutput: function( output ) {
  1639. var type = output.extensions;
  1640. var terminal = new OutputTerminal( { element: this.el, datatypes: type } );
  1641. return terminal;
  1642. },
  1643. } );
  1644. var OutputCollectionTerminalView = BaseOutputTerminalView.extend( {
  1645. terminalMappingClass: OutputCollectionTerminalMapping,
  1646. terminalMappingViewClass: OutputCollectionTerminalMappingView,
  1647. terminalForOutput: function( output ) {
  1648. var collection_type = output.collection_type;
  1649. var terminal = new OutputCollectionTerminal( { element: this.el, collection_type: collection_type, datatypes: output.extensions } );
  1650. return terminal;
  1651. },
  1652. } );
  1653. ////////////
  1654. // END VIEWS
  1655. ////////////
  1656. // FIXME: merge scroll panel into CanvasManager, clean up hardcoded stuff.
  1657. function ScrollPanel( panel ) {
  1658. this.panel = panel;
  1659. }
  1660. $.extend( ScrollPanel.prototype, {
  1661. test: function( e, onmove ) {
  1662. clearTimeout( this.timeout );
  1663. var x = e.pageX,
  1664. y = e.pageY,
  1665. // Panel size and position
  1666. panel = $(this.panel),
  1667. panel_pos = panel.position(),
  1668. panel_w = panel.width(),
  1669. panel_h = panel.height(),
  1670. // Viewport size and offset
  1671. viewport = panel.parent(),
  1672. viewport_w = viewport.width(),
  1673. viewport_h = viewport.height(),
  1674. viewport_offset = viewport.offset(),
  1675. // Edges of viewport (in page coordinates)
  1676. min_x = viewport_offset.left,
  1677. min_y = viewport_offset.top,
  1678. max_x = min_x + viewport.width(),
  1679. max_y = min_y + viewport.height(),
  1680. // Legal panel range
  1681. p_min_x = - ( panel_w - ( viewport_w / 2 ) ),
  1682. p_min_y = - ( panel_h - ( viewport_h / 2 )),
  1683. p_max_x = ( viewport_w / 2 ),
  1684. p_max_y = ( viewport_h / 2 ),
  1685. // Did the panel move?
  1686. moved = false,
  1687. // Constants
  1688. close_dist = 5,
  1689. nudge = 23;
  1690. if ( x - close_dist < min_x ) {
  1691. if ( panel_pos.left < p_max_x ) {
  1692. var t = Math.min( nudge, p_max_x - panel_pos.left );
  1693. panel.css( "left", panel_pos.left + t );
  1694. moved = true;
  1695. }
  1696. } else if ( x + close_dist > max_x ) {
  1697. if ( panel_pos.left > p_min_x ) {
  1698. var t = Math.min( nudge, panel_pos.left - p_min_x );
  1699. panel.css( "left", panel_pos.left - t );
  1700. moved = true;
  1701. }
  1702. } else if ( y - close_dist < min_y ) {
  1703. if ( panel_pos.top < p_max_y ) {
  1704. var t = Math.min( nudge, p_max_y - panel_pos.top );
  1705. panel.css( "top", panel_pos.top + t );
  1706. moved = true;
  1707. }
  1708. } else if ( y + close_dist > max_y ) {
  1709. if ( panel_pos.top > p_min_y ) {
  1710. var t = Math.min( nudge, panel_pos.top - p_min_x );
  1711. panel.css( "top", ( panel_pos.top - t ) + "px" );
  1712. moved = true;
  1713. }
  1714. }
  1715. if ( moved ) {
  1716. // Keep moving even if mouse doesn't move
  1717. onmove();
  1718. var panel = this;
  1719. this.timeout = setTimeout( function() { panel.test( e, onmove ); }, 50 );
  1720. }
  1721. },
  1722. stop: function( e, ui ) {
  1723. clearTimeout( this.timeout );
  1724. }
  1725. });
  1726. function CanvasManager( canvas_viewport, overview ) {
  1727. this.cv = canvas_viewport;
  1728. this.cc = this.cv.find( "#canvas-container" );
  1729. this.oc = overview.find( "#overview-canvas" );
  1730. this.ov = overview.find( "#overview-viewport" );
  1731. // Make overview box draggable
  1732. this.init_drag();
  1733. }
  1734. $.extend( CanvasManager.prototype, {
  1735. init_drag : function () {
  1736. var self = this;
  1737. var move = function( x, y ) {
  1738. x = Math.min( x, self.cv.width() / 2 );
  1739. x = Math.max( x, - self.cc.width() + self.cv.width() / 2 );
  1740. y = Math.min( y, self.cv.height() / 2 );
  1741. y = Math.max( y, - self.cc.height() + self.cv.height() / 2 );
  1742. self.cc.css( {
  1743. left: x,
  1744. top: y
  1745. });
  1746. self.cv.css( { "background-position-x": x,
  1747. "background-position-y": y
  1748. });
  1749. self.update_viewport_overlay();
  1750. };
  1751. // Dragging within canvas background
  1752. this.cc.each( function() {
  1753. this.scroll_panel = new ScrollPanel( this );
  1754. });
  1755. var x_adjust, y_adjust;
  1756. this.cv.bind( "dragstart", function() {
  1757. var o = $(this).offset();
  1758. var p = self.cc.position();
  1759. y_adjust = p.top - o.top;
  1760. x_adjust = p.left - o.left;
  1761. }).bind( "drag", function( e, d ) {
  1762. move( d.offsetX + x_adjust, d.offsetY + y_adjust );
  1763. }).bind( "dragend", function() {
  1764. workflow.fit_canvas_to_nodes();
  1765. self.draw_overview();
  1766. });
  1767. // Dragging for overview pane
  1768. this.ov.bind( "drag", function( e, d ) {
  1769. var in_w = self.cc.width(),
  1770. in_h = self.cc.height(),
  1771. o_w = self.oc.width(),
  1772. o_h = self.oc.height(),
  1773. p = $(this).offsetParent().offset(),
  1774. new_x_offset = d.offsetX - p.left,
  1775. new_y_offset = d.offsetY - p.top;
  1776. move( - ( new_x_offset / o_w * in_w ),
  1777. - ( new_y_offset / o_h * in_h ) );
  1778. }).bind( "dragend", function() {
  1779. workflow.fit_canvas_to_nodes();
  1780. self.draw_overview();
  1781. });
  1782. // Dragging for overview border (resize)
  1783. $("#overview-border").bind( "drag", function( e, d ) {
  1784. var op = $(this).offsetParent();
  1785. var opo = op.offset();
  1786. var new_size = Math.max( op.width() - ( d.offsetX - opo.left ),
  1787. op.height() - ( d.offsetY - opo.top ) );
  1788. $(this).css( {
  1789. width: new_size,
  1790. height: new_size
  1791. });
  1792. self.draw_overview();
  1793. });
  1794. /* Disable dragging for child element of the panel so that resizing can
  1795. only be done by dragging the borders */
  1796. $("#overview-border div").bind("drag", function() { });
  1797. },
  1798. update_viewport_overlay: function() {
  1799. var cc = this.cc,
  1800. cv = this.cv,
  1801. oc = this.oc,
  1802. ov = this.ov,
  1803. in_w = cc.width(),
  1804. in_h = cc.height(),
  1805. o_w = oc.width(),
  1806. o_h = oc.height(),
  1807. cc_pos = cc.position();
  1808. ov.css( {
  1809. left: - ( cc_pos.left / in_w * o_w ),
  1810. top: - ( cc_pos.top / in_h * o_h ),
  1811. // Subtract 2 to account for borders (maybe just change box sizing style instead?)
  1812. width: ( cv.width() / in_w * o_w ) - 2,
  1813. height: ( cv.height() / in_h * o_h ) - 2
  1814. });
  1815. },
  1816. draw_overview: function() {
  1817. var canvas_el = $("#overview-canvas"),
  1818. size = canvas_el.parent().parent().width(),
  1819. c = canvas_el.get(0).getContext("2d"),
  1820. in_w = $("#canvas-container").width(),
  1821. in_h = $("#canvas-container").height();
  1822. var o_h, shift_h, o_w, shift_w;
  1823. // Fit canvas into overview area
  1824. var cv_w = this.cv.width();
  1825. var cv_h = this.cv.height();
  1826. if ( in_w < cv_w && in_h < cv_h ) {
  1827. // Canvas is smaller than viewport
  1828. o_w = in_w / cv_w * size;
  1829. shift_w = ( size - o_w ) / 2;
  1830. o_h = in_h / cv_h * size;
  1831. shift_h = ( size - o_h ) / 2;
  1832. } else if ( in_w < in_h ) {
  1833. // Taller than wide
  1834. shift_h = 0;
  1835. o_h = size;
  1836. o_w = Math.ceil( o_h * in_w / in_h );
  1837. shift_w = ( size - o_w ) / 2;
  1838. } else {
  1839. // Wider than tall
  1840. o_w = size;
  1841. shift_w = 0;
  1842. o_h = Math.ceil( o_w * in_h / in_w );
  1843. shift_h = ( size - o_h ) / 2;
  1844. }
  1845. canvas_el.parent().css( {
  1846. left: shift_w,
  1847. top: shift_h,
  1848. width: o_w,
  1849. height: o_h
  1850. });
  1851. canvas_el.attr( "width", o_w );
  1852. canvas_el.attr( "height", o_h );
  1853. // Draw overview
  1854. $.each( workflow.nodes, function( id, node ) {
  1855. c.fillStyle = "#D2C099";
  1856. c.strokeStyle = "#D8B365";
  1857. c.lineWidth = 1;
  1858. var node_element = $(node.element),
  1859. position = node_element.position(),
  1860. x = position.left / in_w * o_w,
  1861. y = position.top / in_h * o_h,
  1862. w = node_element.width() / in_w * o_w,
  1863. h = node_element.height() / in_h * o_h;
  1864. if (node.tool_errors){
  1865. c.fillStyle = "#FFCCCC";
  1866. c.strokeStyle = "#AA6666";
  1867. } else if (node.workflow_outputs != undefined && node.workflow_outputs.length > 0){
  1868. c.fillStyle = "#E8A92D";
  1869. c.strokeStyle = "#E8A92D";
  1870. }
  1871. c.fillRect( x, y, w, h );
  1872. c.strokeRect( x, y, w, h );
  1873. });
  1874. this.update_viewport_overlay();
  1875. }
  1876. });