'use strict'; var LayoutConstants = require('cose-base').layoutBase.LayoutConstants; var FDLayoutConstants = require('cose-base').layoutBase.FDLayoutConstants; var CoSEConstants = require('cose-base').CoSEConstants; var CoSELayout = require('cose-base').CoSELayout; var CoSENode = require('cose-base').CoSENode; var PointD = require('cose-base').layoutBase.PointD; var DimensionD = require('cose-base').layoutBase.DimensionD; var defaults = { // Called on `layoutready` ready: function () { }, // Called on `layoutstop` stop: function () { }, // 'draft', 'default' or 'proof" // - 'draft' fast cooling rate // - 'default' moderate cooling rate // - "proof" slow cooling rate quality: 'default', // include labels in node dimensions nodeDimensionsIncludeLabels: false, // number of ticks per frame; higher is faster but more jerky refresh: 30, // Whether to fit the network view after when done fit: true, // Padding on fit padding: 10, // Whether to enable incremental mode randomize: true, // Node repulsion (non overlapping) multiplier nodeRepulsion: 4500, // Ideal edge (non nested) length idealEdgeLength: 50, // Divisor to compute edge forces edgeElasticity: 0.45, // Nesting factor (multiplier) to compute ideal edge length for nested edges nestingFactor: 0.1, // Gravity force (constant) gravity: 0.25, // Maximum number of iterations to perform numIter: 2500, // For enabling tiling tile: true, // Type of layout animation. The option set is {'during', 'end', false} animate: 'end', // Duration for animate:end animationDuration: 500, // Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function) tilingPaddingVertical: 10, // Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function) tilingPaddingHorizontal: 10, // Gravity range (constant) for compounds gravityRangeCompound: 1.5, // Gravity force (constant) for compounds gravityCompound: 1.0, // Gravity range (constant) gravityRange: 3.8, // Initial cooling factor for incremental layout initialEnergyOnIncremental: 0.5 }; function extend(defaults, options) { var obj = {}; for (var i in defaults) { obj[i] = defaults[i]; } for (var i in options) { obj[i] = options[i]; } return obj; }; function _CoSELayout(_options) { this.options = extend(defaults, _options); getUserOptions(this.options); } var getUserOptions = function (options) { if (options.nodeRepulsion != null) CoSEConstants.DEFAULT_REPULSION_STRENGTH = FDLayoutConstants.DEFAULT_REPULSION_STRENGTH = options.nodeRepulsion; if (options.idealEdgeLength != null) CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = options.idealEdgeLength; if (options.edgeElasticity != null) CoSEConstants.DEFAULT_SPRING_STRENGTH = FDLayoutConstants.DEFAULT_SPRING_STRENGTH = options.edgeElasticity; if (options.nestingFactor != null) CoSEConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = FDLayoutConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = options.nestingFactor; if (options.gravity != null) CoSEConstants.DEFAULT_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_GRAVITY_STRENGTH = options.gravity; if (options.numIter != null) CoSEConstants.MAX_ITERATIONS = FDLayoutConstants.MAX_ITERATIONS = options.numIter; if (options.gravityRange != null) CoSEConstants.DEFAULT_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_GRAVITY_RANGE_FACTOR = options.gravityRange; if(options.gravityCompound != null) CoSEConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = options.gravityCompound; if(options.gravityRangeCompound != null) CoSEConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = options.gravityRangeCompound; if (options.initialEnergyOnIncremental != null) CoSEConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = options.initialEnergyOnIncremental; if (options.quality == 'draft') LayoutConstants.QUALITY = 0; else if(options.quality == 'proof') LayoutConstants.QUALITY = 2; else LayoutConstants.QUALITY = 1; CoSEConstants.NODE_DIMENSIONS_INCLUDE_LABELS = FDLayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = LayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = options.nodeDimensionsIncludeLabels; CoSEConstants.DEFAULT_INCREMENTAL = FDLayoutConstants.DEFAULT_INCREMENTAL = LayoutConstants.DEFAULT_INCREMENTAL = !(options.randomize); CoSEConstants.ANIMATE = FDLayoutConstants.ANIMATE = LayoutConstants.ANIMATE = options.animate; CoSEConstants.TILE = options.tile; CoSEConstants.TILING_PADDING_VERTICAL = typeof options.tilingPaddingVertical === 'function' ? options.tilingPaddingVertical.call() : options.tilingPaddingVertical; CoSEConstants.TILING_PADDING_HORIZONTAL = typeof options.tilingPaddingHorizontal === 'function' ? options.tilingPaddingHorizontal.call() : options.tilingPaddingHorizontal; }; _CoSELayout.prototype.run = function () { var ready; var frameId; var options = this.options; var idToLNode = this.idToLNode = {}; var layout = this.layout = new CoSELayout(); var self = this; self.stopped = false; this.cy = this.options.cy; this.cy.trigger({ type: 'layoutstart', layout: this }); var gm = layout.newGraphManager(); this.gm = gm; var nodes = this.options.eles.nodes(); var edges = this.options.eles.edges(); this.root = gm.addRoot(); this.processChildrenList(this.root, this.getTopMostNodes(nodes), layout); for (var i = 0; i < edges.length; i++) { var edge = edges[i]; var sourceNode = this.idToLNode[edge.data("source")]; var targetNode = this.idToLNode[edge.data("target")]; if(sourceNode !== targetNode && sourceNode.getEdgesBetween(targetNode).length == 0){ var e1 = gm.add(layout.newEdge(), sourceNode, targetNode); e1.id = edge.id(); } } var getPositions = function(ele, i){ if(typeof ele === "number") { ele = i; } var theId = ele.data('id'); var lNode = self.idToLNode[theId]; return { x: lNode.getRect().getCenterX(), y: lNode.getRect().getCenterY() }; }; /* * Reposition nodes in iterations animatedly */ var iterateAnimated = function () { // Thigs to perform after nodes are repositioned on screen var afterReposition = function() { if (options.fit) { options.cy.fit(options.eles, options.padding); } if (!ready) { ready = true; self.cy.one('layoutready', options.ready); self.cy.trigger({type: 'layoutready', layout: self}); } }; var ticksPerFrame = self.options.refresh; var isDone; for( var i = 0; i < ticksPerFrame && !isDone; i++ ){ isDone = self.stopped || self.layout.tick(); } // If layout is done if (isDone) { // If the layout is not a sublayout and it is successful perform post layout. if (layout.checkLayoutSuccess() && !layout.isSubLayout) { layout.doPostLayout(); } // If layout has a tilingPostLayout function property call it. if (layout.tilingPostLayout) { layout.tilingPostLayout(); } layout.isLayoutFinished = true; self.options.eles.nodes().positions(getPositions); afterReposition(); // trigger layoutstop when the layout stops (e.g. finishes) self.cy.one('layoutstop', self.options.stop); self.cy.trigger({ type: 'layoutstop', layout: self }); if (frameId) { cancelAnimationFrame(frameId); } ready = false; return; } var animationData = self.layout.getPositionsData(); // Get positions of layout nodes note that all nodes may not be layout nodes because of tiling // Position nodes, for the nodes whose id does not included in data (because they are removed from their parents and included in dummy compounds) // use position of their ancestors or dummy ancestors options.eles.nodes().positions(function (ele, i) { if (typeof ele === "number") { ele = i; } // If ele is a compound node, then its position will be defined by its children if(!ele.isParent()){ var theId = ele.id(); var pNode = animationData[theId]; var temp = ele; // If pNode is undefined search until finding position data of its first ancestor (It may be dummy as well) while (pNode == null) { pNode = animationData[temp.data('parent')] || animationData['DummyCompound_' + temp.data('parent')]; animationData[theId] = pNode; temp = temp.parent()[0]; if(temp == undefined){ break; } } if(pNode != null){ return { x: pNode.x, y: pNode.y }; } else{ return { x: ele.position('x'), y: ele.position('y') }; } } }); afterReposition(); frameId = requestAnimationFrame(iterateAnimated); }; /* * Listen 'layoutstarted' event and start animated iteration if animate option is 'during' */ layout.addListener('layoutstarted', function () { if (self.options.animate === 'during') { frameId = requestAnimationFrame(iterateAnimated); } }); layout.runLayout(); // Run cose layout /* * If animate option is not 'during' ('end' or false) perform these here (If it is 'during' similar things are already performed) */ if(this.options.animate !== "during"){ self.options.eles.nodes().not(":parent").layoutPositions(self, self.options, getPositions); // Use layout positions to reposition the nodes it considers the options parameter ready = false; } return this; // chaining }; //Get the top most ones of a list of nodes _CoSELayout.prototype.getTopMostNodes = function(nodes) { var nodesMap = {}; for (var i = 0; i < nodes.length; i++) { nodesMap[nodes[i].id()] = true; } var roots = nodes.filter(function (ele, i) { if(typeof ele === "number") { ele = i; } var parent = ele.parent()[0]; while(parent != null){ if(nodesMap[parent.id()]){ return false; } parent = parent.parent()[0]; } return true; }); return roots; }; _CoSELayout.prototype.processChildrenList = function (parent, children, layout) { var size = children.length; for (var i = 0; i < size; i++) { var theChild = children[i]; var children_of_children = theChild.children(); var theNode; var dimensions = theChild.layoutDimensions({ nodeDimensionsIncludeLabels: this.options.nodeDimensionsIncludeLabels }); if (theChild.outerWidth() != null && theChild.outerHeight() != null) { theNode = parent.add(new CoSENode(layout.graphManager, new PointD(theChild.position('x') - dimensions.w / 2, theChild.position('y') - dimensions.h / 2), new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h)))); } else { theNode = parent.add(new CoSENode(this.graphManager)); } // Attach id to the layout node theNode.id = theChild.data("id"); // Attach the paddings of cy node to layout node theNode.paddingLeft = parseInt( theChild.css('padding') ); theNode.paddingTop = parseInt( theChild.css('padding') ); theNode.paddingRight = parseInt( theChild.css('padding') ); theNode.paddingBottom = parseInt( theChild.css('padding') ); //Attach the label properties to compound if labels will be included in node dimensions if(this.options.nodeDimensionsIncludeLabels){ if(theChild.isParent()){ var labelWidth = theChild.boundingBox({ includeLabels: true, includeNodes: false }).w; var labelHeight = theChild.boundingBox({ includeLabels: true, includeNodes: false }).h; var labelPos = theChild.css("text-halign"); theNode.labelWidth = labelWidth; theNode.labelHeight = labelHeight; theNode.labelPos = labelPos; } } // Map the layout node this.idToLNode[theChild.data("id")] = theNode; if (isNaN(theNode.rect.x)) { theNode.rect.x = 0; } if (isNaN(theNode.rect.y)) { theNode.rect.y = 0; } if (children_of_children != null && children_of_children.length > 0) { var theNewGraph; theNewGraph = layout.getGraphManager().add(layout.newGraph(), theNode); this.processChildrenList(theNewGraph, children_of_children, layout); } } }; /** * @brief : called on continuous layouts to stop them before they finish */ _CoSELayout.prototype.stop = function () { this.stopped = true; return this; // chaining }; var register = function( cytoscape ){ // var Layout = getLayout( cytoscape ); cytoscape('layout', 'cose-bilkent', _CoSELayout); }; // auto reg for globals if( typeof cytoscape !== 'undefined' ){ register( cytoscape ); } module.exports = register;