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

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

https://bitbucket.org/kellrott/galaxy-central
JavaScript | 1973 lines | 1710 code | 130 blank | 133 comment | 237 complexity | b2bb68ccfa5d7806903e0e72880ea568 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.connected() ) {
  285. inputFilled = false;
  286. } else {
  287. var firstOutput = this.connectors[ 0 ].handle1;
  288. if( ! firstOutput ){
  289. inputFilled = false;
  290. } else {
  291. if( firstOutput.isDataCollectionInput || firstOutput.isMappedOver() || firstOutput.datatypes.indexOf( "input_collection" ) > 0 ) {
  292. inputFilled = true;
  293. } else {
  294. inputFilled = false;
  295. }
  296. }
  297. }
  298. } else {
  299. inputFilled = true;
  300. }
  301. }
  302. return inputFilled;
  303. },
  304. _mappingConstraints: function( ) {
  305. // If this is a connected terminal, return list of collection types
  306. // other terminals connected to node are constraining mapping to.
  307. if( ! this.node ) {
  308. return []; // No node - completely unconstrained
  309. }
  310. var mapOver = this.mapOver();
  311. if( mapOver.isCollection ) {
  312. return [ mapOver ];
  313. }
  314. var constraints = [];
  315. if( ! this.node.hasConnectedOutputTerminals() ) {
  316. _.each( this.node.connectedMappedInputTerminals(), function( inputTerminal ) {
  317. constraints.push( inputTerminal.mapOver() );
  318. } );
  319. } else {
  320. // All outputs should have same mapOver status - least specific.
  321. constraints.push( _.first( _.values( this.node.output_terminals ) ).mapOver() );
  322. }
  323. return constraints;
  324. },
  325. _producesAcceptableDatatype: function( other ) {
  326. // other is a non-collection output...
  327. for ( var t in this.datatypes ) {
  328. var cat_outputs = new Array();
  329. cat_outputs = cat_outputs.concat(other.datatypes);
  330. if (other.node.post_job_actions){
  331. for (var pja_i in other.node.post_job_actions){
  332. var pja = other.node.post_job_actions[pja_i];
  333. if (pja.action_type == "ChangeDatatypeAction" && (pja.output_name == '' || pja.output_name == other.name) && pja.action_arguments){
  334. cat_outputs.push(pja.action_arguments['newtype']);
  335. }
  336. }
  337. }
  338. // FIXME: No idea what to do about case when datatype is 'input'
  339. for ( var other_datatype_i in cat_outputs ) {
  340. var other_datatype = cat_outputs[other_datatype_i];
  341. if ( other_datatype == "input" || other_datatype == "input_collection" || issubtype( cat_outputs[other_datatype_i], this.datatypes[t] ) ) {
  342. return true;
  343. }
  344. }
  345. }
  346. return false;
  347. },
  348. _otherCollectionType: function( other ) {
  349. var otherCollectionType = NULL_COLLECTION_TYPE_DESCRIPTION;
  350. if( other.isDataCollectionInput ) {
  351. otherCollectionType = other.collectionType;
  352. } else {
  353. var otherMapOver = other.mapOver();
  354. if( otherMapOver.isCollection ) {
  355. otherCollectionType = otherMapOver;
  356. }
  357. }
  358. return otherCollectionType;
  359. },
  360. } );
  361. var InputTerminal = BaseInputTerminal.extend( {
  362. update: function( input ) {
  363. this.datatypes = input.extensions;
  364. this.multiple = input.multiple;
  365. this.collection = false;
  366. },
  367. connect: function( connector ) {
  368. BaseInputTerminal.prototype.connect.call( this, connector );
  369. var other_output = connector.handle1;
  370. if( ! other_output ) {
  371. return;
  372. }
  373. var otherCollectionType = this._otherCollectionType( other_output );
  374. if( otherCollectionType.isCollection ) {
  375. this.setMapOver( otherCollectionType );
  376. }
  377. },
  378. attachable: function( other ) {
  379. var otherCollectionType = this._otherCollectionType( other );
  380. var thisMapOver = this.mapOver();
  381. if( otherCollectionType.isCollection ) {
  382. if( this.multiple ) {
  383. if( this.connected() ) {
  384. return false;
  385. }
  386. if( otherCollectionType.rank == 1 ) {
  387. return this._producesAcceptableDatatype( other );
  388. } else {
  389. // TODO: Allow subcollection mapping over this as if it were
  390. // a list collection input.
  391. return false;
  392. }
  393. }
  394. if( thisMapOver.isCollection && thisMapOver.canMatch( otherCollectionType ) ) {
  395. return this._producesAcceptableDatatype( other );
  396. } else {
  397. // Need to check if this would break constraints...
  398. var mappingConstraints = this._mappingConstraints();
  399. if( mappingConstraints.every( _.bind( otherCollectionType.canMatch, otherCollectionType ) ) ) {
  400. return this._producesAcceptableDatatype( other );
  401. } else {
  402. return false;
  403. }
  404. }
  405. } else if( thisMapOver.isCollection ) {
  406. // Attempting to match a non-collection output to an
  407. // explicitly collection input.
  408. return false;
  409. }
  410. return this._producesAcceptableDatatype( other );
  411. }
  412. });
  413. var InputCollectionTerminal = BaseInputTerminal.extend( {
  414. update: function( input ) {
  415. this.multiple = false;
  416. this.collection = true;
  417. this.datatypes = input.extensions;
  418. if( input.collection_type ) {
  419. this.collectionType = new CollectionTypeDescription( input.collection_type );
  420. } else {
  421. this.collectionType = ANY_COLLECTION_TYPE_DESCRIPTION;
  422. }
  423. },
  424. connect: function( connector ) {
  425. BaseInputTerminal.prototype.connect.call( this, connector );
  426. var other = connector.handle1;
  427. if( ! other ) {
  428. return;
  429. }
  430. var effectiveMapOver = this._effectiveMapOver( other );
  431. this.setMapOver( effectiveMapOver );
  432. },
  433. _effectiveMapOver: function( other ) {
  434. var collectionType = this.collectionType;
  435. var otherCollectionType = this._otherCollectionType( other );
  436. if( ! collectionType.canMatch( otherCollectionType ) ) {
  437. return otherCollectionType.effectiveMapOver( collectionType );
  438. } else {
  439. return NULL_COLLECTION_TYPE_DESCRIPTION;
  440. }
  441. },
  442. _effectiveCollectionType: function( ) {
  443. var collectionType = this.collectionType;
  444. var thisMapOver = this.mapOver();
  445. return thisMapOver.append( collectionType );
  446. },
  447. attachable: function ( other ) {
  448. var otherCollectionType = this._otherCollectionType( other );
  449. if( otherCollectionType.isCollection ) {
  450. var effectiveCollectionType = this._effectiveCollectionType( );
  451. var thisMapOver = this.mapOver();
  452. if( effectiveCollectionType.canMatch( otherCollectionType ) ) {
  453. // Only way a direct match...
  454. return this._producesAcceptableDatatype( other );
  455. // Otherwise we need to mapOver
  456. } else if( thisMapOver.isCollection ) {
  457. // In this case, mapOver already set and we didn't match skipping...
  458. return false;
  459. } else if( otherCollectionType.canMapOver( this.collectionType ) ) {
  460. var effectiveMapOver = this._effectiveMapOver( other );
  461. if( ! effectiveMapOver.isCollection ) {
  462. return false;
  463. }
  464. // Need to check if this would break constraints...
  465. var mappingConstraints = this._mappingConstraints();
  466. if( mappingConstraints.every( effectiveMapOver.canMatch ) ) {
  467. return this._producesAcceptableDatatype( other );
  468. }
  469. }
  470. }
  471. return false;
  472. }
  473. });
  474. var OutputCollectionTerminal = Terminal.extend( {
  475. initialize: function( attr ) {
  476. Terminal.prototype.initialize.call( this, attr );
  477. this.datatypes = attr.datatypes;
  478. this.collectionType = new CollectionTypeDescription( attr.collection_type );
  479. this.isDataCollectionInput = true;
  480. },
  481. update: function( output ) {
  482. var newCollectionType = new CollectionTypeDescription( output.collection_type );
  483. if( newCollectionType.collectionType != this.collectionType.collectionType ) {
  484. _.each( this.connectors, function( connector ) {
  485. // TODO: consider checking if connection valid before removing...
  486. connector.destroy();
  487. } );
  488. }
  489. this.collectionType = newCollectionType;
  490. }
  491. } );
  492. function Connector( handle1, handle2 ) {
  493. this.canvas = null;
  494. this.dragging = false;
  495. this.inner_color = "#FFFFFF";
  496. this.outer_color = "#D8B365";
  497. if ( handle1 && handle2 ) {
  498. this.connect( handle1, handle2 );
  499. }
  500. }
  501. $.extend( Connector.prototype, {
  502. connect: function ( t1, t2 ) {
  503. this.handle1 = t1;
  504. if ( this.handle1 ) {
  505. this.handle1.connect( this );
  506. }
  507. this.handle2 = t2;
  508. if ( this.handle2 ) {
  509. this.handle2.connect( this );
  510. }
  511. },
  512. destroy : function () {
  513. if ( this.handle1 ) {
  514. this.handle1.disconnect( this );
  515. }
  516. if ( this.handle2 ) {
  517. this.handle2.disconnect( this );
  518. }
  519. $(this.canvas).remove();
  520. },
  521. destroyIfInvalid: function() {
  522. if( this.handle1 && this.handle2 && ! this.handle2.attachable( this.handle1 ) ) {
  523. this.destroy();
  524. }
  525. },
  526. redraw : function () {
  527. var canvas_container = $("#canvas-container");
  528. if ( ! this.canvas ) {
  529. this.canvas = document.createElement( "canvas" );
  530. // excanvas specific hack
  531. if ( window.G_vmlCanvasManager ) {
  532. G_vmlCanvasManager.initElement( this.canvas );
  533. }
  534. canvas_container.append( $(this.canvas) );
  535. if ( this.dragging ) {
  536. this.canvas.style.zIndex = "300";
  537. }
  538. }
  539. var relativeLeft = function( e ) {
  540. return $(e).offset().left - canvas_container.offset().left;
  541. };
  542. var relativeTop = function( e ) {
  543. return $(e).offset().top - canvas_container.offset().top;
  544. };
  545. if (!this.handle1 || !this.handle2) {
  546. return;
  547. }
  548. // Find the position of each handle
  549. var start_x = relativeLeft( this.handle1.element ) + 5;
  550. var start_y = relativeTop( this.handle1.element ) + 5;
  551. var end_x = relativeLeft( this.handle2.element ) + 5;
  552. var end_y = relativeTop( this.handle2.element ) + 5;
  553. // Calculate canvas area
  554. var canvas_extra = 100;
  555. var canvas_min_x = Math.min( start_x, end_x );
  556. var canvas_max_x = Math.max( start_x, end_x );
  557. var canvas_min_y = Math.min( start_y, end_y );
  558. var canvas_max_y = Math.max( start_y, end_y );
  559. var cp_shift = Math.min( Math.max( Math.abs( canvas_max_y - canvas_min_y ) / 2, 100 ), 300 );
  560. var canvas_left = canvas_min_x - canvas_extra;
  561. var canvas_top = canvas_min_y - canvas_extra;
  562. var canvas_width = canvas_max_x - canvas_min_x + 2 * canvas_extra;
  563. var canvas_height = canvas_max_y - canvas_min_y + 2 * canvas_extra;
  564. // Place the canvas
  565. this.canvas.style.left = canvas_left + "px";
  566. this.canvas.style.top = canvas_top + "px";
  567. this.canvas.setAttribute( "width", canvas_width );
  568. this.canvas.setAttribute( "height", canvas_height );
  569. // Adjust points to be relative to the canvas
  570. start_x -= canvas_left;
  571. start_y -= canvas_top;
  572. end_x -= canvas_left;
  573. end_y -= canvas_top;
  574. // Draw the line
  575. var c = this.canvas.getContext("2d"),
  576. start_offsets = null,
  577. end_offsets = null;
  578. var num_offsets = 1;
  579. if ( this.handle1 && this.handle1.isMappedOver() ) {
  580. var start_offsets = [ -6, -3, 0, 3, 6 ];
  581. num_offsets = 5;
  582. } else {
  583. var start_offsets = [ 0 ];
  584. }
  585. if ( this.handle2 && this.handle2.isMappedOver() ) {
  586. var end_offsets = [ -6, -3, 0, 3, 6 ];
  587. num_offsets = 5;
  588. } else {
  589. var end_offsets = [ 0 ];
  590. }
  591. var connector = this;
  592. for( var i = 0; i < num_offsets; i++ ) {
  593. var inner_width = 5,
  594. outer_width = 7;
  595. if( start_offsets.length > 1 || end_offsets.length > 1 ) {
  596. // We have a multi-run, using many lines, make them small.
  597. inner_width = 1;
  598. outer_width = 3;
  599. }
  600. 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 ] );
  601. }
  602. },
  603. draw_outlined_curve : function( start_x, start_y, end_x, end_y, cp_shift, inner_width, outer_width, offset_start, offset_end ) {
  604. var offset_start = offset_start || 0;
  605. var offset_end = offset_end || 0;
  606. var c = this.canvas.getContext("2d");
  607. c.lineCap = "round";
  608. c.strokeStyle = this.outer_color;
  609. c.lineWidth = outer_width;
  610. c.beginPath();
  611. c.moveTo( start_x, start_y + offset_start );
  612. c.bezierCurveTo( start_x + cp_shift, start_y + offset_start, end_x - cp_shift, end_y + offset_end, end_x, end_y + offset_end);
  613. c.stroke();
  614. // Inner line
  615. c.strokeStyle = this.inner_color;
  616. c.lineWidth = inner_width;
  617. c.beginPath();
  618. c.moveTo( start_x, start_y + offset_start );
  619. c.bezierCurveTo( start_x + cp_shift, start_y + offset_start, end_x - cp_shift, end_y + offset_end, end_x, end_y + offset_end );
  620. c.stroke();
  621. }
  622. } );
  623. var Node = Backbone.Model.extend({
  624. initialize: function( attr ) {
  625. this.element = attr.element;
  626. this.input_terminals = {};
  627. this.output_terminals = {};
  628. this.tool_errors = {};
  629. },
  630. connectedOutputTerminals: function() {
  631. return this._connectedTerminals( this.output_terminals );
  632. },
  633. _connectedTerminals: function( terminals ) {
  634. var connectedTerminals = [];
  635. $.each( terminals, function( _, t ) {
  636. if( t.connectors.length > 0 ) {
  637. connectedTerminals.push( t );
  638. }
  639. } );
  640. return connectedTerminals;
  641. },
  642. hasConnectedOutputTerminals: function() {
  643. // return this.connectedOutputTerminals().length > 0; <- optimized this
  644. var outputTerminals = this.output_terminals;
  645. for( var outputName in outputTerminals ) {
  646. if( outputTerminals[ outputName ].connectors.length > 0 ) {
  647. return true;
  648. }
  649. }
  650. return false;
  651. },
  652. connectedMappedInputTerminals: function() {
  653. return this._connectedMappedTerminals( this.input_terminals );
  654. },
  655. hasConnectedMappedInputTerminals: function() {
  656. // return this.connectedMappedInputTerminals().length > 0; <- optimized this
  657. var inputTerminals = this.input_terminals;
  658. for( var inputName in inputTerminals ) {
  659. var inputTerminal = inputTerminals[ inputName ];
  660. if( inputTerminal.connectors.length > 0 && inputTerminal.isMappedOver() ) {
  661. return true;
  662. }
  663. }
  664. return false;
  665. },
  666. _connectedMappedTerminals: function( terminals ) {
  667. var mapped_outputs = [];
  668. $.each( terminals, function( _, t ) {
  669. var mapOver = t.mapOver();
  670. if( mapOver.isCollection ) {
  671. if( t.connectors.length > 0 ) {
  672. mapped_outputs.push( t );
  673. }
  674. }
  675. });
  676. return mapped_outputs;
  677. },
  678. mappedInputTerminals: function() {
  679. return this._mappedTerminals( this.input_terminals );
  680. },
  681. _mappedTerminals: function( terminals ) {
  682. var mappedTerminals = [];
  683. $.each( terminals, function( _, t ) {
  684. var mapOver = t.mapOver();
  685. if( mapOver.isCollection ) {
  686. mappedTerminals.push( t );
  687. }
  688. } );
  689. return mappedTerminals;
  690. },
  691. hasMappedOverInputTerminals: function() {
  692. var found = false;
  693. _.each( this.input_terminals, function( t ) {
  694. var mapOver = t.mapOver();
  695. if( mapOver.isCollection ) {
  696. found = true;
  697. }
  698. } );
  699. return found;
  700. },
  701. redraw : function () {
  702. $.each( this.input_terminals, function( _, t ) {
  703. t.redraw();
  704. });
  705. $.each( this.output_terminals, function( _, t ) {
  706. t.redraw();
  707. });
  708. },
  709. destroy : function () {
  710. $.each( this.input_terminals, function( k, t ) {
  711. t.destroy();
  712. });
  713. $.each( this.output_terminals, function( k, t ) {
  714. t.destroy();
  715. });
  716. workflow.remove_node( this );
  717. $(this.element).remove();
  718. },
  719. make_active : function () {
  720. $(this.element).addClass( "toolForm-active" );
  721. },
  722. make_inactive : function () {
  723. // Keep inactive nodes stacked from most to least recently active
  724. // by moving element to the end of parent's node list
  725. var element = this.element.get(0);
  726. (function(p) { p.removeChild( element ); p.appendChild( element ); })(element.parentNode);
  727. // Remove active class
  728. $(element).removeClass( "toolForm-active" );
  729. },
  730. init_field_data : function ( data ) {
  731. if ( data.type ) {
  732. this.type = data.type;
  733. }
  734. this.name = data.name;
  735. this.form_html = data.form_html;
  736. this.tool_state = data.tool_state;
  737. this.tool_errors = data.tool_errors;
  738. this.tooltip = data.tooltip ? data.tooltip : "";
  739. this.annotation = data.annotation;
  740. this.post_job_actions = data.post_job_actions ? data.post_job_actions : {};
  741. this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : [];
  742. var node = this;
  743. var nodeView = new NodeView({
  744. el: this.element[ 0 ],
  745. node: node,
  746. });
  747. node.nodeView = nodeView;
  748. $.each( data.data_inputs, function( i, input ) {
  749. nodeView.addDataInput( input );
  750. });
  751. if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) {
  752. nodeView.addRule();
  753. }
  754. $.each( data.data_outputs, function( i, output ) {
  755. nodeView.addDataOutput( output );
  756. } );
  757. nodeView.render();
  758. workflow.node_changed( this );
  759. },
  760. update_field_data : function( data ) {
  761. var node = this;
  762. nodeView = node.nodeView;
  763. this.tool_state = data.tool_state;
  764. this.form_html = data.form_html;
  765. this.tool_errors = data.tool_errors;
  766. this.annotation = data['annotation'];
  767. if( "post_job_actions" in data ) {
  768. // Won't be present in response for data inputs
  769. var pja_in = $.parseJSON(data.post_job_actions);
  770. this.post_job_actions = pja_in ? pja_in : {};
  771. }
  772. node.nodeView.renderToolErrors();
  773. // Update input rows
  774. var old_body = nodeView.$( "div.inputs" );
  775. var new_body = nodeView.newInputsDiv();
  776. var newTerminalViews = {};
  777. _.each( data.data_inputs, function( input ) {
  778. var terminalView = node.nodeView.addDataInput( input, new_body );
  779. newTerminalViews[ input.name ] = terminalView;
  780. });
  781. // Cleanup any leftover terminals
  782. _.each( _.difference( _.values( nodeView.terminalViews ), _.values( newTerminalViews ) ), function( unusedView ) {
  783. unusedView.el.terminal.destroy();
  784. } );
  785. nodeView.terminalViews = newTerminalViews;
  786. // In general workflow editor assumes tool outputs don't change in # or
  787. // type (not really valid right?) but adding special logic here for
  788. // data collection input parameters that can have their collection
  789. // change.
  790. if( data.data_outputs.length == 1 && "collection_type" in data.data_outputs[ 0 ] ) {
  791. nodeView.updateDataOutput( data.data_outputs[ 0 ] );
  792. }
  793. old_body.replaceWith( new_body );
  794. // If active, reactivate with new form_html
  795. this.markChanged();
  796. this.redraw();
  797. },
  798. error : function ( text ) {
  799. var b = $(this.element).find( ".toolFormBody" );
  800. b.find( "div" ).remove();
  801. var tmp = "<div style='color: red; text-style: italic;'>" + text + "</div>";
  802. this.form_html = tmp;
  803. b.html( tmp );
  804. workflow.node_changed( this );
  805. },
  806. markChanged: function() {
  807. workflow.node_changed( this );
  808. }
  809. } );
  810. function Workflow( canvas_container ) {
  811. this.canvas_container = canvas_container;
  812. this.id_counter = 0;
  813. this.nodes = {};
  814. this.name = null;
  815. this.has_changes = false;
  816. this.active_form_has_changes = false;
  817. }
  818. $.extend( Workflow.prototype, {
  819. add_node : function( node ) {
  820. node.id = this.id_counter;
  821. node.element.attr( 'id', 'wf-node-step-' + node.id );
  822. this.id_counter++;
  823. this.nodes[ node.id ] = node;
  824. this.has_changes = true;
  825. node.workflow = this;
  826. },
  827. remove_node : function( node ) {
  828. if ( this.active_node == node ) {
  829. this.clear_active_node();
  830. }
  831. delete this.nodes[ node.id ] ;
  832. this.has_changes = true;
  833. },
  834. remove_all : function() {
  835. wf = this;
  836. $.each( this.nodes, function ( k, v ) {
  837. v.destroy();
  838. wf.remove_node( v );
  839. });
  840. },
  841. rectify_workflow_outputs : function() {
  842. // Find out if we're using workflow_outputs or not.
  843. var using_workflow_outputs = false;
  844. var has_existing_pjas = false;
  845. $.each( this.nodes, function ( k, node ) {
  846. if (node.workflow_outputs && node.workflow_outputs.length > 0){
  847. using_workflow_outputs = true;
  848. }
  849. $.each(node.post_job_actions, function(pja_id, pja){
  850. if (pja.action_type === "HideDatasetAction"){
  851. has_existing_pjas = true;
  852. }
  853. });
  854. });
  855. if (using_workflow_outputs !== false || has_existing_pjas !== false){
  856. // Using workflow outputs, or has existing pjas. Remove all PJAs and recreate based on outputs.
  857. $.each(this.nodes, function (k, node ){
  858. if (node.type === 'tool'){
  859. var node_changed = false;
  860. if (node.post_job_actions == null){
  861. node.post_job_actions = {};
  862. node_changed = true;
  863. }
  864. var pjas_to_rem = [];
  865. $.each(node.post_job_actions, function(pja_id, pja){
  866. if (pja.action_type == "HideDatasetAction"){
  867. pjas_to_rem.push(pja_id);
  868. }
  869. });
  870. if (pjas_to_rem.length > 0 ) {
  871. $.each(pjas_to_rem, function(i, pja_name){
  872. node_changed = true;
  873. delete node.post_job_actions[pja_name];
  874. });
  875. }
  876. if (using_workflow_outputs){
  877. $.each(node.output_terminals, function(ot_id, ot){
  878. var create_pja = true;
  879. $.each(node.workflow_outputs, function(i, wo_name){
  880. if (ot.name === wo_name){
  881. create_pja = false;
  882. }
  883. });
  884. if (create_pja === true){
  885. node_changed = true;
  886. var pja = {
  887. action_type : "HideDatasetAction",
  888. output_name : ot.name,
  889. action_arguments : {}
  890. }
  891. node.post_job_actions['HideDatasetAction'+ot.name] = null;
  892. node.post_job_actions['HideDatasetAction'+ot.name] = pja;
  893. }
  894. });
  895. }
  896. // lastly, if this is the active node, and we made changes, reload the display at right.
  897. if (workflow.active_node == node && node_changed === true) {
  898. workflow.reload_active_node();
  899. }
  900. }
  901. });
  902. }
  903. },
  904. to_simple : function () {
  905. var nodes = {};
  906. $.each( this.nodes, function ( i, node ) {
  907. var input_connections = {};
  908. $.each( node.input_terminals, function ( k, t ) {
  909. input_connections[ t.name ] = null;
  910. // There should only be 0 or 1 connectors, so this is
  911. // really a sneaky if statement
  912. var cons = []
  913. $.each( t.connectors, function ( i, c ) {
  914. cons[i] = { id: c.handle1.node.id, output_name: c.handle1.name };
  915. input_connections[ t.name ] = cons;
  916. });
  917. });
  918. var post_job_actions = {};
  919. if (node.post_job_actions){
  920. $.each( node.post_job_actions, function ( i, act ) {
  921. var pja = {
  922. action_type : act.action_type,
  923. output_name : act.output_name,
  924. action_arguments : act.action_arguments
  925. }
  926. post_job_actions[ act.action_type + act.output_name ] = null;
  927. post_job_actions[ act.action_type + act.output_name ] = pja;
  928. });
  929. }
  930. if (!node.workflow_outputs){
  931. node.workflow_outputs = [];
  932. // Just in case.
  933. }
  934. var node_data = {
  935. id : node.id,
  936. type : node.type,
  937. tool_id : node.tool_id,
  938. tool_state : node.tool_state,
  939. tool_errors : node.tool_errors,
  940. input_connections : input_connections,
  941. position : $(node.element).position(),
  942. annotation: node.annotation,
  943. post_job_actions: node.post_job_actions,
  944. workflow_outputs: node.workflow_outputs
  945. };
  946. nodes[ node.id ] = node_data;
  947. });
  948. return { steps: nodes };
  949. },
  950. from_simple : function ( data ) {
  951. wf = this;
  952. var max_id = 0;
  953. wf.name = data.name;
  954. // First pass, nodes
  955. var using_workflow_outputs = false;
  956. $.each( data.steps, function( id, step ) {
  957. var node = prebuild_node( step.type, step.name, step.tool_id );
  958. node.init_field_data( step );
  959. if ( step.position ) {
  960. node.element.css( { top: step.position.top, left: step.position.left } );
  961. }
  962. node.id = step.id;
  963. wf.nodes[ node.id ] = node;
  964. max_id = Math.max( max_id, parseInt( id ) );
  965. // For older workflows, it's possible to have HideDataset PJAs, but not WorkflowOutputs.
  966. // Check for either, and then add outputs in the next pass.
  967. if (!using_workflow_outputs && node.type === 'tool'){
  968. if (node.workflow_outputs.length > 0){
  969. using_workflow_outputs = true;
  970. }
  971. else{
  972. $.each(node.post_job_actions, function(pja_id, pja){
  973. if (pja.action_type === "HideDatasetAction"){
  974. using_workflow_outputs = true;
  975. }
  976. });
  977. }
  978. }
  979. });
  980. wf.id_counter = max_id + 1;
  981. // Second pass, connections
  982. $.each( data.steps, function( id, step ) {
  983. var node = wf.nodes[id];
  984. $.each( step.input_connections, function( k, v ) {
  985. if ( v ) {
  986. if ( ! $.isArray( v ) ) {
  987. v = [ v ];
  988. }
  989. $.each( v, function( l, x ) {
  990. var other_node = wf.nodes[ x.id ];
  991. var c = new Connector();
  992. c.connect( other_node.output_terminals[ x.output_name ],
  993. node.input_terminals[ k ] );
  994. c.redraw();
  995. });
  996. }
  997. });
  998. if(using_workflow_outputs && node.type === 'tool'){
  999. // Ensure that every output terminal has a WorkflowOutput or HideDatasetAction.
  1000. $.each(node.output_terminals, function(ot_id, ot){
  1001. if(node.post_job_actions['HideDatasetAction'+ot.name] === undefined){
  1002. node.workflow_outputs.push(ot.name);
  1003. callout = $(node.element).find('.callout.'+ot.name);
  1004. callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png');
  1005. workflow.has_changes = true;
  1006. }
  1007. });
  1008. }
  1009. });
  1010. },
  1011. check_changes_in_active_form : function() {
  1012. // If active form has changed, save it
  1013. if (this.active_form_has_changes) {
  1014. this.has_changes = true;
  1015. // Submit form.
  1016. $("#right-content").find("form").submit();
  1017. this.active_form_has_changes = false;
  1018. }
  1019. },
  1020. reload_active_node : function() {
  1021. if (this.active_node){
  1022. var node = this.active_node;
  1023. this.clear_active_node();
  1024. this.activate_node(node);
  1025. }
  1026. },
  1027. clear_active_node : function() {
  1028. if ( this.active_node ) {
  1029. this.active_node.make_inactive();
  1030. this.active_node = null;
  1031. }
  1032. parent.show_form_for_tool( "<div>No node selected</div>" );
  1033. },
  1034. activate_node : function( node ) {
  1035. if ( this.active_node != node ) {
  1036. this.check_changes_in_active_form();
  1037. this.clear_active_node();
  1038. parent.show_form_for_tool( node.form_html + node.tooltip, node );
  1039. node.make_active();
  1040. this.active_node = node;
  1041. }
  1042. },
  1043. node_changed : function ( node ) {
  1044. this.has_changes = true;
  1045. if ( this.active_node == node ) {
  1046. // Reactive with new form_html
  1047. this.check_changes_in_active_form(); //Force changes to be saved even on new connection (previously dumped)
  1048. parent.show_form_for_tool( node.form_html + node.tooltip, node );
  1049. }
  1050. },
  1051. layout : function () {
  1052. this.check_changes_in_active_form();
  1053. this.has_changes = true;
  1054. // Prepare predecessor / successor tracking
  1055. var n_pred = {};
  1056. var successors = {};
  1057. // First pass to initialize arrays even for nodes with no connections
  1058. $.each( this.nodes, function( id, node ) {
  1059. if ( n_pred[id] === undefined ) { n_pred[id] = 0; }
  1060. if ( successors[id] === undefined ) { successors[id] = []; }
  1061. });
  1062. // Second pass to count predecessors and successors
  1063. $.each( this.nodes, function( id, node ) {
  1064. $.each( node.input_terminals, function ( j, t ) {
  1065. $.each( t.connectors, function ( k, c ) {
  1066. // A connection exists from `other` to `node`
  1067. var other = c.handle1.node;
  1068. // node gains a predecessor
  1069. n_pred[node.id] += 1;
  1070. // other gains a successor
  1071. successors[other.id].push( node.id );
  1072. });
  1073. });
  1074. });
  1075. // Assemble order, tracking levels
  1076. node_ids_by_level = [];
  1077. while ( true ) {
  1078. // Everything without a predecessor
  1079. level_parents = [];
  1080. for ( var pred_k in n_pred ) {
  1081. if ( n_pred[ pred_k ] == 0 ) {
  1082. level_parents.push( pred_k );
  1083. }
  1084. }
  1085. if ( level_parents.length == 0 ) {
  1086. break;
  1087. }
  1088. node_ids_by_level.push( level_parents );
  1089. // Remove the parents from this level, and decrement the number
  1090. // of predecessors for each successor
  1091. for ( var k in level_parents ) {
  1092. var v = level_parents[k];
  1093. delete n_pred[v];
  1094. for ( var sk in successors[v] ) {
  1095. n_pred[ successors[v][sk] ] -= 1;
  1096. }
  1097. }
  1098. }
  1099. if ( n_pred.length ) {
  1100. // ERROR: CYCLE! Currently we do nothing
  1101. return;
  1102. }
  1103. // Layout each level
  1104. var all_nodes = this.nodes;
  1105. var h_pad = 80; v_pad = 30;
  1106. var left = h_pad;
  1107. $.each( node_ids_by_level, function( i, ids ) {
  1108. // We keep nodes in the same order in a level to give the user
  1109. // some control over ordering
  1110. ids.sort( function( a, b ) {
  1111. return $(all_nodes[a].element).position().top - $(all_nodes[b].element).position().top;
  1112. });
  1113. // Position each node
  1114. var max_width = 0;
  1115. var top = v_pad;
  1116. $.each( ids, function( j, id ) {
  1117. var node = all_nodes[id];
  1118. var element = $(node.element);
  1119. $(element).css( { top: top, left: left } );
  1120. max_width = Math.max( max_width, $(element).width() );
  1121. top += $(element).height() + v_pad;
  1122. });
  1123. left += max_width + h_pad;
  1124. });
  1125. // Need to redraw all connectors
  1126. $.each( all_nodes, function( _, node ) { node.redraw(); } );
  1127. },
  1128. bounds_for_all_nodes: function() {
  1129. var xmin = Infinity, xmax = -Infinity,
  1130. ymin = Infinity, ymax = -Infinity,
  1131. p;
  1132. $.each( this.nodes, function( id, node ) {
  1133. e = $(node.element);
  1134. p = e.position();
  1135. xmin = Math.min( xmin, p.left );
  1136. xmax = Math.max( xmax, p.left + e.width() );
  1137. ymin = Math.min( ymin, p.top );
  1138. ymax = Math.max( ymax, p.top + e.width() );
  1139. });
  1140. return { xmin: xmin, xmax: xmax, ymin: ymin, ymax: ymax };
  1141. },
  1142. fit_canvas_to_nodes: function() {
  1143. // Span of all elements
  1144. var bounds = this.bounds_for_all_nodes();
  1145. var position = this.canvas_container.position();
  1146. var parent = this.canvas_container.parent();
  1147. // Determine amount we need to expand on top/left
  1148. var xmin_delta = fix_delta( bounds.xmin, 100 );
  1149. var ymin_delta = fix_delta( bounds.ymin, 100 );
  1150. // May need to expand farther to fill viewport
  1151. xmin_delta = Math.max( xmin_delta, position.left );
  1152. ymin_delta = Math.max( ymin_delta, position.top );
  1153. var left = position.left - xmin_delta;
  1154. var top = position.top - ymin_delta;
  1155. // Same for width/height
  1156. var width = round_up( bounds.xmax + 100, 100 ) + xmin_delta;
  1157. var height = round_up( bounds.ymax + 100, 100 ) + ymin_delta;
  1158. width = Math.max( width, - left + parent.width() );
  1159. height = Math.max( height, - top + parent.height() );
  1160. // Grow the canvas container
  1161. this.canvas_container.css( {
  1162. left: left,
  1163. top: top,
  1164. width: width,
  1165. height: height
  1166. });
  1167. // Move elements back if needed
  1168. this.canvas_container.children().each( function() {
  1169. var p = $(this).position();
  1170. $(this).css( "left", p.left + xmin_delta );
  1171. $(this).css( "top", p.top + ymin_delta );
  1172. });
  1173. }
  1174. });
  1175. function fix_delta( x, n ) {
  1176. if ( x < n|| x > 3*n ) {
  1177. new_pos = ( Math.ceil( ( ( x % n ) ) / n ) + 1 ) * n;
  1178. return ( - ( x - new_pos ) );
  1179. }
  1180. return 0;
  1181. }
  1182. function round_up( x, n ) {
  1183. return Math.ceil( x / n ) * n;
  1184. }
  1185. function prebuild_node( type, title_text, tool_id ) {
  1186. var f = $("<div class='toolForm toolFormInCanvas'></div>");
  1187. var node = new Node( { element: f } );
  1188. node.type = type;
  1189. if ( type == 'tool' ) {
  1190. node.tool_id = tool_id;
  1191. }
  1192. var title = $("<div class='toolFormTitle unselectable'>" + title_text + "</div>" );
  1193. f.append( title );
  1194. f.css( "left", $(window).scrollLeft() + 20 ); f.css( "top", $(window).scrollTop() + 20 );
  1195. var b = $("<div class='toolFormBody'></div>");
  1196. var tmp = "<div><img height='16' align='middle' src='" + galaxy_config.root + "static/images/loading_small_white_bg.gif'/> loading tool info...</div>";
  1197. b.append( tmp );
  1198. node.form_html = tmp;
  1199. f.append( b );
  1200. // Fix width to computed width
  1201. // Now add floats
  1202. var buttons = $("<div class='buttons' style='float: right;'></div>");
  1203. buttons.append( $("<div>").addClass("fa-icon-button fa fa-times").click( function( e ) {
  1204. node.destroy();
  1205. }));
  1206. // Place inside container
  1207. f.appendTo( "#canvas-container" );
  1208. // Position in container
  1209. var o = $("#canvas-container").position();
  1210. var p = $("#canvas-container").parent();
  1211. var width = f.width();
  1212. var height = f.height();
  1213. f.css( { left: ( - o.left ) + ( p.width() / 2 ) - ( width / 2 ), top: ( - o.top ) + ( p.height() / 2 ) - ( height / 2 ) } );
  1214. buttons.prependTo( title );
  1215. width += ( buttons.width() + 10 );
  1216. f.css( "width", width );
  1217. $(f).bind( "dragstart", function() {
  1218. workflow.activate_node( node );
  1219. }).bind( "dragend", function() {
  1220. workflow.node_changed( this );
  1221. workflow.fit_canvas_to_nodes();
  1222. canvas_manager.draw_overview();
  1223. }).bind( "dragclickonly", function() {
  1224. workflow.activate_node( node );
  1225. }).bind( "drag", function( e, d ) {
  1226. // Move
  1227. var po = $(this).offsetParent().offset(),
  1228. x = d.offsetX - po.left,
  1229. y = d.offsetY - po.top;
  1230. $(this).css( { left: x, top: y } );
  1231. // Redraw
  1232. $(this).find( ".terminal" ).each( function() {
  1233. this.terminal.redraw();
  1234. });
  1235. });
  1236. return node;
  1237. }
  1238. function add_node( type, title_text, tool_id ) {
  1239. // Abstraction for use by galaxy.workflow.js to hide
  1240. // some editor details from workflow code and reduce duplication
  1241. // between add_node_for_tool and add_node_for_module.
  1242. var node = prebuild_node( type, title_text, tool_id );
  1243. workflow.add_node( node );
  1244. workflow.fit_canvas_to_nodes();
  1245. canvas_manager.draw_overview();
  1246. workflow.activate_node( node );
  1247. return node;
  1248. }
  1249. var ext_to_type = null;
  1250. var type_to_type = null;
  1251. function issubtype( child, parent ) {
  1252. child = ext_to_type[child];
  1253. parent = ext_to_type[parent];
  1254. return ( type_to_type[child] ) && ( parent in type_to_type[child] );
  1255. }
  1256. function populate_datatype_info( data ) {
  1257. ext_to_type = data.ext_to_class_name;
  1258. type_to_type = data.class_to_classes;
  1259. }
  1260. //////////////
  1261. // START VIEWS
  1262. //////////////
  1263. var NodeView = Backbone.View.extend( {
  1264. initialize: function( options ){
  1265. this.node = options.node;
  1266. this.output_width = Math

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