PageRenderTime 70ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

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

https://bitbucket.org/hbc/galaxy-central-hbc/
JavaScript | 1991 lines | 1720 code | 130 blank | 141 comment | 241 complexity | c6f8619e1882812047aa9b91508c5d54 MD5 | raw file
Possible License(s): CC-BY-3.0

Large files files are truncated, but you can click here to view the full file

  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_

Large files files are truncated, but you can click here to view the full file