UNPKG

cose-base

Version:

Core module for compound spring embedder based layout styles

1,393 lines (1,207 loc) 71.6 kB
var FDLayout = require('layout-base').FDLayout; var CoSEGraphManager = require('./CoSEGraphManager'); var CoSEGraph = require('./CoSEGraph'); var CoSENode = require('./CoSENode'); var CoSEEdge = require('./CoSEEdge'); var CoSEConstants = require('./CoSEConstants'); var ConstraintHandler = require('./ConstraintHandler'); var FDLayoutConstants = require('layout-base').FDLayoutConstants; var LayoutConstants = require('layout-base').LayoutConstants; var Point = require('layout-base').Point; var PointD = require('layout-base').PointD; var DimensionD = require('layout-base').DimensionD; var Layout = require('layout-base').Layout; var Integer = require('layout-base').Integer; var IGeometry = require('layout-base').IGeometry; var LGraph = require('layout-base').LGraph; var Transform = require('layout-base').Transform; var LinkedList = require('layout-base').LinkedList; function CoSELayout() { FDLayout.call(this); this.toBeTiled = {}; // Memorize if a node is to be tiled or is tiled this.constraints = {}; // keep layout constraints } CoSELayout.prototype = Object.create(FDLayout.prototype); for (var prop in FDLayout) { CoSELayout[prop] = FDLayout[prop]; } CoSELayout.prototype.newGraphManager = function () { var gm = new CoSEGraphManager(this); this.graphManager = gm; return gm; }; CoSELayout.prototype.newGraph = function (vGraph) { return new CoSEGraph(null, this.graphManager, vGraph); }; CoSELayout.prototype.newNode = function (vNode) { return new CoSENode(this.graphManager, vNode); }; CoSELayout.prototype.newEdge = function (vEdge) { return new CoSEEdge(null, null, vEdge); }; CoSELayout.prototype.initParameters = function () { FDLayout.prototype.initParameters.call(this, arguments); if (!this.isSubLayout) { if (CoSEConstants.DEFAULT_EDGE_LENGTH < 10) { this.idealEdgeLength = 10; } else { this.idealEdgeLength = CoSEConstants.DEFAULT_EDGE_LENGTH; } this.useSmartIdealEdgeLengthCalculation = CoSEConstants.DEFAULT_USE_SMART_IDEAL_EDGE_LENGTH_CALCULATION; this.gravityConstant = FDLayoutConstants.DEFAULT_GRAVITY_STRENGTH; this.compoundGravityConstant = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH; this.gravityRangeFactor = FDLayoutConstants.DEFAULT_GRAVITY_RANGE_FACTOR; this.compoundGravityRangeFactor = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR; // variables for tree reduction support this.prunedNodesAll = []; this.growTreeIterations = 0; this.afterGrowthIterations = 0; this.isTreeGrowing = false; this.isGrowthFinished = false; } }; // This method is used to set CoSE related parameters used by spring embedder. CoSELayout.prototype.initSpringEmbedder = function () { FDLayout.prototype.initSpringEmbedder.call(this); // variables for cooling this.coolingCycle = 0; this.maxCoolingCycle = this.maxIterations/FDLayoutConstants.CONVERGENCE_CHECK_PERIOD; this.finalTemperature = 0.04; this.coolingAdjuster = 1; }; CoSELayout.prototype.layout = function () { var createBendsAsNeeded = LayoutConstants.DEFAULT_CREATE_BENDS_AS_NEEDED; if (createBendsAsNeeded) { this.createBendpoints(); this.graphManager.resetAllEdges(); } this.level = 0; return this.classicLayout(); }; CoSELayout.prototype.classicLayout = function () { this.nodesWithGravity = this.calculateNodesToApplyGravitationTo(); this.graphManager.setAllNodesToApplyGravitation(this.nodesWithGravity); this.calcNoOfChildrenForAllNodes(); this.graphManager.calcLowestCommonAncestors(); this.graphManager.calcInclusionTreeDepths(); this.graphManager.getRoot().calcEstimatedSize(); this.calcIdealEdgeLengths(); if (!this.incremental) { var forest = this.getFlatForest(); // The graph associated with this layout is flat and a forest if (forest.length > 0) { this.positionNodesRadially(forest); } // The graph associated with this layout is not flat or a forest else { // Reduce the trees when incremental mode is not enabled and graph is not a forest this.reduceTrees(); // Update nodes that gravity will be applied this.graphManager.resetAllNodesToApplyGravitation(); var allNodes = new Set(this.getAllNodes()); var intersection = this.nodesWithGravity.filter(x => allNodes.has(x)); this.graphManager.setAllNodesToApplyGravitation(intersection); this.positionNodesRandomly(); } } else { if(CoSEConstants.TREE_REDUCTION_ON_INCREMENTAL){ // Reduce the trees in incremental mode if only this constant is set to true this.reduceTrees(); // Update nodes that gravity will be applied this.graphManager.resetAllNodesToApplyGravitation(); var allNodes = new Set(this.getAllNodes()); var intersection = this.nodesWithGravity.filter(x => allNodes.has(x)); this.graphManager.setAllNodesToApplyGravitation(intersection); } } if(Object.keys(this.constraints).length > 0){ ConstraintHandler.handleConstraints(this); this.initConstraintVariables(); } this.initSpringEmbedder(); if (CoSEConstants.APPLY_LAYOUT) { this.runSpringEmbedder(); } return true; }; CoSELayout.prototype.tick = function() { this.totalIterations++; if (this.totalIterations === this.maxIterations && !this.isTreeGrowing && !this.isGrowthFinished) { if(this.prunedNodesAll.length > 0){ this.isTreeGrowing = true; } else { return true; } } if (this.totalIterations % FDLayoutConstants.CONVERGENCE_CHECK_PERIOD == 0 && !this.isTreeGrowing && !this.isGrowthFinished) { if (this.isConverged()) { if(this.prunedNodesAll.length > 0){ this.isTreeGrowing = true; } else { return true; } } this.coolingCycle++; if(this.layoutQuality == 0) { // quality - "draft" this.coolingAdjuster = this.coolingCycle; } else if(this.layoutQuality == 1) { // quality - "default" this.coolingAdjuster = this.coolingCycle / 3; } // cooling schedule is based on http://www.btluke.com/simanf1.html -> cooling schedule 3 this.coolingFactor = Math.max(this.initialCoolingFactor - Math.pow(this.coolingCycle, Math.log(100 * (this.initialCoolingFactor - this.finalTemperature)) / Math.log(this.maxCoolingCycle))/100 * this.coolingAdjuster, this.finalTemperature); this.animationPeriod = Math.ceil(this.initialAnimationPeriod * Math.sqrt(this.coolingFactor)); } // Operations while tree is growing again if(this.isTreeGrowing){ if(this.growTreeIterations % 10 == 0){ if(this.prunedNodesAll.length > 0) { this.graphManager.updateBounds(); this.updateGrid(); this.growTree(this.prunedNodesAll); // Update nodes that gravity will be applied this.graphManager.resetAllNodesToApplyGravitation(); var allNodes = new Set(this.getAllNodes()); var intersection = this.nodesWithGravity.filter(x => allNodes.has(x)); this.graphManager.setAllNodesToApplyGravitation(intersection); this.graphManager.updateBounds(); this.updateGrid(); if(CoSEConstants.PURE_INCREMENTAL) this.coolingFactor = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL / 2; else this.coolingFactor = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL; } else { this.isTreeGrowing = false; this.isGrowthFinished = true; } } this.growTreeIterations++; } // Operations after growth is finished if(this.isGrowthFinished){ if (this.isConverged()) { return true; } if(this.afterGrowthIterations % 10 == 0){ this.graphManager.updateBounds(); this.updateGrid(); } if(CoSEConstants.PURE_INCREMENTAL) this.coolingFactor = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL / 2 * ((100 - this.afterGrowthIterations) / 100); else this.coolingFactor = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL * ((100 - this.afterGrowthIterations) / 100); this.afterGrowthIterations++; } var gridUpdateAllowed = !this.isTreeGrowing && !this.isGrowthFinished; var forceToNodeSurroundingUpdate = (this.growTreeIterations % 10 == 1 && this.isTreeGrowing) || (this.afterGrowthIterations % 10 == 1 && this.isGrowthFinished); this.totalDisplacement = 0; this.graphManager.updateBounds(); this.calcSpringForces(); this.calcRepulsionForces(gridUpdateAllowed, forceToNodeSurroundingUpdate); this.calcGravitationalForces(); this.moveNodes(); this.animate(); return false; // Layout is not ended yet return false }; CoSELayout.prototype.getPositionsData = function() { var allNodes = this.graphManager.getAllNodes(); var pData = {}; for (var i = 0; i < allNodes.length; i++) { var rect = allNodes[i].rect; var id = allNodes[i].id; pData[id] = { id: id, x: rect.getCenterX(), y: rect.getCenterY(), w: rect.width, h: rect.height }; } return pData; }; CoSELayout.prototype.runSpringEmbedder = function () { this.initialAnimationPeriod = 25; this.animationPeriod = this.initialAnimationPeriod; var layoutEnded = false; // If aminate option is 'during' signal that layout is supposed to start iterating if ( FDLayoutConstants.ANIMATE === 'during' ) { this.emit('layoutstarted'); } else { // If aminate option is 'during' tick() function will be called on index.js while (!layoutEnded) { layoutEnded = this.tick(); } this.graphManager.updateBounds(); } }; // overrides moveNodes method in FDLayout CoSELayout.prototype.moveNodes = function () { var lNodes = this.getAllNodes(); var node; // calculate displacement for each node for (var i = 0; i < lNodes.length; i++) { node = lNodes[i]; node.calculateDisplacement(); } if(Object.keys(this.constraints).length > 0){ this.updateDisplacements(); } // move each node for (var i = 0; i < lNodes.length; i++) { node = lNodes[i]; node.move(); } }; // constraint related methods: initConstraintVariables and updateDisplacements // initialize constraint related variables CoSELayout.prototype.initConstraintVariables = function () { var self = this; this.idToNodeMap = new Map(); this.fixedNodeSet = new Set(); var allNodes = this.graphManager.getAllNodes(); // fill idToNodeMap for(var i = 0; i < allNodes.length; i++) { var node = allNodes[i]; this.idToNodeMap.set(node.id, node); } // calculate fixed node weight for given compound node var calculateCompoundWeight = function(compoundNode) { var nodes = compoundNode.getChild().getNodes(); var node; var fixedNodeWeight = 0; for (var i = 0; i < nodes.length; i++) { node = nodes[i]; if (node.getChild() == null) { if(self.fixedNodeSet.has(node.id)) { fixedNodeWeight += 100; } } else { fixedNodeWeight += calculateCompoundWeight(node); } } return fixedNodeWeight; }; if (this.constraints.fixedNodeConstraint) { // fill fixedNodeSet this.constraints.fixedNodeConstraint.forEach(function (nodeData) { self.fixedNodeSet.add(nodeData.nodeId); }); // assign fixed node weights to compounds if they contain fixed nodes var allNodes = this.graphManager.getAllNodes(); var node; for (var i = 0; i < allNodes.length; i++) { node = allNodes[i]; if (node.getChild() != null) { let fixedNodeWeight = calculateCompoundWeight(node); if (fixedNodeWeight > 0) { node.fixedNodeWeight = fixedNodeWeight; } } } } if(this.constraints.relativePlacementConstraint) { var nodeToDummyForVerticalAlignment = new Map(); var nodeToDummyForHorizontalAlignment = new Map(); this.dummyToNodeForVerticalAlignment = new Map(); this.dummyToNodeForHorizontalAlignment = new Map(); this.fixedNodesOnHorizontal = new Set(); this.fixedNodesOnVertical = new Set(); // fill maps and sets this.fixedNodeSet.forEach(function(nodeId){ self.fixedNodesOnHorizontal.add(nodeId); self.fixedNodesOnVertical.add(nodeId); }); if(this.constraints.alignmentConstraint) { if(this.constraints.alignmentConstraint.vertical) { var verticalAlignment = this.constraints.alignmentConstraint.vertical; for(var i = 0; i < verticalAlignment.length; i++) { this.dummyToNodeForVerticalAlignment.set("dummy" + i, []); verticalAlignment[i].forEach(function(nodeId){ nodeToDummyForVerticalAlignment.set(nodeId, "dummy" + i); self.dummyToNodeForVerticalAlignment.get("dummy" + i).push(nodeId); if(self.fixedNodeSet.has(nodeId)) { self.fixedNodesOnHorizontal.add("dummy" + i); } }); } } if(this.constraints.alignmentConstraint.horizontal){ var horizontalAlignment = this.constraints.alignmentConstraint.horizontal; for(var i = 0; i < horizontalAlignment.length; i++){ this.dummyToNodeForHorizontalAlignment.set("dummy" + i, []); horizontalAlignment[i].forEach(function(nodeId){ nodeToDummyForHorizontalAlignment.set(nodeId, "dummy" + i); self.dummyToNodeForHorizontalAlignment.get("dummy" + i).push(nodeId); if(self.fixedNodeSet.has(nodeId)){ self.fixedNodesOnVertical.add("dummy" + i); } }); } } } if(CoSEConstants.RELAX_MOVEMENT_ON_CONSTRAINTS) { this.shuffle = function (array) { var j, x, i; for (i = array.length - 1; i >= (2 * array.length / 3); i--) { j = Math.floor(Math.random() * (i + 1)); x = array[i]; array[i] = array[j]; array[j] = x; } return array; }; this.nodesInRelativeHorizontal = []; this.nodesInRelativeVertical = []; this.nodeToRelativeConstraintMapHorizontal = new Map(); this.nodeToRelativeConstraintMapVertical = new Map(); this.nodeToTempPositionMapHorizontal = new Map(); this.nodeToTempPositionMapVertical = new Map(); // fill arrays and maps this.constraints.relativePlacementConstraint.forEach(function(constraint) { if(constraint.left) { let nodeIdLeft = nodeToDummyForVerticalAlignment.has(constraint.left) ? nodeToDummyForVerticalAlignment.get(constraint.left) : constraint.left; let nodeIdRight = nodeToDummyForVerticalAlignment.has(constraint.right) ? nodeToDummyForVerticalAlignment.get(constraint.right) : constraint.right; if(!self.nodesInRelativeHorizontal.includes(nodeIdLeft)) { self.nodesInRelativeHorizontal.push(nodeIdLeft); self.nodeToRelativeConstraintMapHorizontal.set(nodeIdLeft, []); if(self.dummyToNodeForVerticalAlignment.has(nodeIdLeft)) { self.nodeToTempPositionMapHorizontal.set(nodeIdLeft, self.idToNodeMap.get(self.dummyToNodeForVerticalAlignment.get(nodeIdLeft)[0]).getCenterX()); } else { self.nodeToTempPositionMapHorizontal.set(nodeIdLeft, self.idToNodeMap.get(nodeIdLeft).getCenterX()); } } if(!self.nodesInRelativeHorizontal.includes(nodeIdRight)) { self.nodesInRelativeHorizontal.push(nodeIdRight); self.nodeToRelativeConstraintMapHorizontal.set(nodeIdRight, []); if(self.dummyToNodeForVerticalAlignment.has(nodeIdRight)) { self.nodeToTempPositionMapHorizontal.set(nodeIdRight, self.idToNodeMap.get(self.dummyToNodeForVerticalAlignment.get(nodeIdRight)[0]).getCenterX()); } else { self.nodeToTempPositionMapHorizontal.set(nodeIdRight, self.idToNodeMap.get(nodeIdRight).getCenterX()); } } self.nodeToRelativeConstraintMapHorizontal.get(nodeIdLeft).push({right: nodeIdRight, gap: constraint.gap}); self.nodeToRelativeConstraintMapHorizontal.get(nodeIdRight).push({left: nodeIdLeft, gap: constraint.gap}); } else { let nodeIdTop = nodeToDummyForHorizontalAlignment.has(constraint.top) ? nodeToDummyForHorizontalAlignment.get(constraint.top) : constraint.top; let nodeIdBottom = nodeToDummyForHorizontalAlignment.has(constraint.bottom) ? nodeToDummyForHorizontalAlignment.get(constraint.bottom) : constraint.bottom; if(!self.nodesInRelativeVertical.includes(nodeIdTop)) { self.nodesInRelativeVertical.push(nodeIdTop); self.nodeToRelativeConstraintMapVertical.set(nodeIdTop, []); if(self.dummyToNodeForHorizontalAlignment.has(nodeIdTop)) { self.nodeToTempPositionMapVertical.set(nodeIdTop, self.idToNodeMap.get(self.dummyToNodeForHorizontalAlignment.get(nodeIdTop)[0]).getCenterY()); } else { self.nodeToTempPositionMapVertical.set(nodeIdTop, self.idToNodeMap.get(nodeIdTop).getCenterY()); } } if(!self.nodesInRelativeVertical.includes(nodeIdBottom)) { self.nodesInRelativeVertical.push(nodeIdBottom); self.nodeToRelativeConstraintMapVertical.set(nodeIdBottom, []); if(self.dummyToNodeForHorizontalAlignment.has(nodeIdBottom)) { self.nodeToTempPositionMapVertical.set(nodeIdBottom, self.idToNodeMap.get(self.dummyToNodeForHorizontalAlignment.get(nodeIdBottom)[0]).getCenterY()); } else { self.nodeToTempPositionMapVertical.set(nodeIdBottom, self.idToNodeMap.get(nodeIdBottom).getCenterY()); } } self.nodeToRelativeConstraintMapVertical.get(nodeIdTop).push({bottom: nodeIdBottom, gap: constraint.gap}); self.nodeToRelativeConstraintMapVertical.get(nodeIdBottom).push({top: nodeIdTop, gap: constraint.gap}); } }); } else { var subGraphOnHorizontal = new Map(); // subgraph from vertical RP constraints var subGraphOnVertical = new Map(); // subgraph from vertical RP constraints // construct subgraphs from relative placement constraints this.constraints.relativePlacementConstraint.forEach(function(constraint) { if(constraint.left) { var left = nodeToDummyForVerticalAlignment.has(constraint.left) ? nodeToDummyForVerticalAlignment.get(constraint.left) : constraint.left; var right = nodeToDummyForVerticalAlignment.has(constraint.right) ? nodeToDummyForVerticalAlignment.get(constraint.right) : constraint.right; if(subGraphOnHorizontal.has(left)) { subGraphOnHorizontal.get(left).push(right); } else { subGraphOnHorizontal.set(left, [right]); } if(subGraphOnHorizontal.has(right)) { subGraphOnHorizontal.get(right).push(left); } else { subGraphOnHorizontal.set(right, [left]); } } else { var top = nodeToDummyForHorizontalAlignment.has(constraint.top) ? nodeToDummyForHorizontalAlignment.get(constraint.top) : constraint.top; var bottom = nodeToDummyForHorizontalAlignment.has(constraint.bottom) ? nodeToDummyForHorizontalAlignment.get(constraint.bottom) : constraint.bottom; if(subGraphOnVertical.has(top)) { subGraphOnVertical.get(top).push(bottom); } else { subGraphOnVertical.set(top, [bottom]); } if(subGraphOnVertical.has(bottom)) { subGraphOnVertical.get(bottom).push(top); } else { subGraphOnVertical.set(bottom, [top]); } } }); // function to construct components from a given graph // also returns an array that keeps whether each component contains fixed node var constructComponents = function(graph, fixedNodes){ let components = []; let isFixed = []; let queue = new LinkedList(); let visited = new Set(); let count = 0; graph.forEach(function(value, key){ if(!visited.has(key)){ components[count] = []; isFixed[count] = false; let currentNode = key; queue.push(currentNode); visited.add(currentNode); components[count].push(currentNode); while(queue.length != 0){ currentNode = queue.shift(); if(fixedNodes.has(currentNode)) { isFixed[count] = true; } let neighbors = graph.get(currentNode); neighbors.forEach(function(neighbor){ if(!visited.has(neighbor)){ queue.push(neighbor); visited.add(neighbor); components[count].push(neighbor); } }); } count++; } }); return { components: components, isFixed: isFixed }; }; var resultOnHorizontal = constructComponents(subGraphOnHorizontal, self.fixedNodesOnHorizontal); this.componentsOnHorizontal = resultOnHorizontal.components; this.fixedComponentsOnHorizontal = resultOnHorizontal.isFixed; var resultOnVertical = constructComponents(subGraphOnVertical, self.fixedNodesOnVertical); this.componentsOnVertical = resultOnVertical.components; this.fixedComponentsOnVertical = resultOnVertical.isFixed; } } }; // updates node displacements based on constraints CoSELayout.prototype.updateDisplacements = function () { var self = this; if(this.constraints.fixedNodeConstraint){ this.constraints.fixedNodeConstraint.forEach(function(nodeData){ var fixedNode = self.idToNodeMap.get(nodeData.nodeId); fixedNode.displacementX = 0; fixedNode.displacementY = 0; }); } if(this.constraints.alignmentConstraint){ if(this.constraints.alignmentConstraint.vertical){ var allVerticalAlignments = this.constraints.alignmentConstraint.vertical; for(var i = 0; i < allVerticalAlignments.length; i++){ var totalDisplacementX = 0; for(var j = 0; j < allVerticalAlignments[i].length; j++){ if(this.fixedNodeSet.has(allVerticalAlignments[i][j])){ totalDisplacementX = 0; break; } totalDisplacementX += this.idToNodeMap.get(allVerticalAlignments[i][j]).displacementX; } var averageDisplacementX = totalDisplacementX / allVerticalAlignments[i].length; for(var j = 0; j < allVerticalAlignments[i].length; j++){ this.idToNodeMap.get(allVerticalAlignments[i][j]).displacementX = averageDisplacementX; } } } if(this.constraints.alignmentConstraint.horizontal){ var allHorizontalAlignments = this.constraints.alignmentConstraint.horizontal; for(var i = 0; i < allHorizontalAlignments.length; i++){ var totalDisplacementY = 0; for(var j = 0; j < allHorizontalAlignments[i].length; j++){ if(this.fixedNodeSet.has(allHorizontalAlignments[i][j])){ totalDisplacementY = 0; break; } totalDisplacementY += this.idToNodeMap.get(allHorizontalAlignments[i][j]).displacementY; } var averageDisplacementY = totalDisplacementY / allHorizontalAlignments[i].length; for(var j = 0; j < allHorizontalAlignments[i].length; j++){ this.idToNodeMap.get(allHorizontalAlignments[i][j]).displacementY = averageDisplacementY; } } } } if(this.constraints.relativePlacementConstraint){ if(CoSEConstants.RELAX_MOVEMENT_ON_CONSTRAINTS) { // shuffle array to randomize node processing order if (this.totalIterations % 10 == 0) { this.shuffle(this.nodesInRelativeHorizontal); this.shuffle(this.nodesInRelativeVertical); } this.nodesInRelativeHorizontal.forEach(function(nodeId) { if(!self.fixedNodesOnHorizontal.has(nodeId)) { var displacement = 0; if(self.dummyToNodeForVerticalAlignment.has(nodeId)) { displacement = self.idToNodeMap.get(self.dummyToNodeForVerticalAlignment.get(nodeId)[0]).displacementX; } else { displacement = self.idToNodeMap.get(nodeId).displacementX; } self.nodeToRelativeConstraintMapHorizontal.get(nodeId).forEach(function(constraint) { if(constraint.right) { var diff = (self.nodeToTempPositionMapHorizontal.get(constraint.right) - self.nodeToTempPositionMapHorizontal.get(nodeId)) - displacement; if(diff < constraint.gap) { displacement -= constraint.gap - diff; } } else { var diff = (self.nodeToTempPositionMapHorizontal.get(nodeId) - self.nodeToTempPositionMapHorizontal.get(constraint.left)) + displacement; if(diff < constraint.gap) { displacement += constraint.gap - diff; } } }); self.nodeToTempPositionMapHorizontal.set(nodeId, self.nodeToTempPositionMapHorizontal.get(nodeId) + displacement); if(self.dummyToNodeForVerticalAlignment.has(nodeId)) { self.dummyToNodeForVerticalAlignment.get(nodeId).forEach(function(nodeId){ self.idToNodeMap.get(nodeId).displacementX = displacement; }); } else { self.idToNodeMap.get(nodeId).displacementX = displacement; } } }); this.nodesInRelativeVertical.forEach(function(nodeId) { if(!self.fixedNodesOnHorizontal.has(nodeId)) { var displacement = 0; if(self.dummyToNodeForHorizontalAlignment.has(nodeId)) { displacement = self.idToNodeMap.get(self.dummyToNodeForHorizontalAlignment.get(nodeId)[0]).displacementY; } else { displacement = self.idToNodeMap.get(nodeId).displacementY; } self.nodeToRelativeConstraintMapVertical.get(nodeId).forEach(function(constraint) { if(constraint.bottom) { var diff = (self.nodeToTempPositionMapVertical.get(constraint.bottom) - self.nodeToTempPositionMapVertical.get(nodeId)) - displacement; if(diff < constraint.gap) { displacement -= constraint.gap - diff; } } else { var diff = (self.nodeToTempPositionMapVertical.get(nodeId) - self.nodeToTempPositionMapVertical.get(constraint.top)) + displacement; if(diff < constraint.gap) { displacement += constraint.gap - diff; } } }); self.nodeToTempPositionMapVertical.set(nodeId, self.nodeToTempPositionMapVertical.get(nodeId) + displacement); if(self.dummyToNodeForHorizontalAlignment.has(nodeId)) { self.dummyToNodeForHorizontalAlignment.get(nodeId).forEach(function(nodeId){ self.idToNodeMap.get(nodeId).displacementY = displacement; }); } else { self.idToNodeMap.get(nodeId).displacementY = displacement; } } }); } else { for(var i = 0; i < this.componentsOnHorizontal.length; i++) { var component = this.componentsOnHorizontal[i]; if(this.fixedComponentsOnHorizontal[i]) { for(var j = 0; j < component.length; j++){ if(this.dummyToNodeForVerticalAlignment.has(component[j])) { this.dummyToNodeForVerticalAlignment.get(component[j]).forEach(function(nodeId){ self.idToNodeMap.get(nodeId).displacementX = 0; }); } else { this.idToNodeMap.get(component[j]).displacementX = 0; } } } else { var sum = 0; var count = 0; for(var j = 0; j < component.length; j++){ if(this.dummyToNodeForVerticalAlignment.has(component[j])) { var actualNodes = this.dummyToNodeForVerticalAlignment.get(component[j]); sum += actualNodes.length * this.idToNodeMap.get(actualNodes[0]).displacementX; count += actualNodes.length; } else { sum += this.idToNodeMap.get(component[j]).displacementX; count++; } } var averageDisplacement = sum / count; for(var j = 0; j < component.length; j++){ if(this.dummyToNodeForVerticalAlignment.has(component[j])) { this.dummyToNodeForVerticalAlignment.get(component[j]).forEach(function(nodeId){ self.idToNodeMap.get(nodeId).displacementX = averageDisplacement; }); } else { this.idToNodeMap.get(component[j]).displacementX = averageDisplacement; } } } } for(var i = 0; i < this.componentsOnVertical.length; i++) { var component = this.componentsOnVertical[i]; if(this.fixedComponentsOnVertical[i]) { for(var j = 0; j < component.length; j++){ if(this.dummyToNodeForHorizontalAlignment.has(component[j])) { this.dummyToNodeForHorizontalAlignment.get(component[j]).forEach(function(nodeId){ self.idToNodeMap.get(nodeId).displacementY = 0; }); } else { this.idToNodeMap.get(component[j]).displacementY = 0; } } } else { var sum = 0; var count = 0; for(var j = 0; j < component.length; j++){ if(this.dummyToNodeForHorizontalAlignment.has(component[j])) { var actualNodes = this.dummyToNodeForHorizontalAlignment.get(component[j]); sum += actualNodes.length * this.idToNodeMap.get(actualNodes[0]).displacementY; count += actualNodes.length; } else { sum += this.idToNodeMap.get(component[j]).displacementY; count++; } } var averageDisplacement = sum / count; for(var j = 0; j < component.length; j++){ if(this.dummyToNodeForHorizontalAlignment.has(component[j])) { this.dummyToNodeForHorizontalAlignment.get(component[j]).forEach(function(nodeId){ self.idToNodeMap.get(nodeId).displacementY = averageDisplacement; }); } else { this.idToNodeMap.get(component[j]).displacementY = averageDisplacement; } } } } } } }; CoSELayout.prototype.calculateNodesToApplyGravitationTo = function () { var nodeList = []; var graph; var graphs = this.graphManager.getGraphs(); var size = graphs.length; var i; for (i = 0; i < size; i++) { graph = graphs[i]; graph.updateConnected(); if (!graph.isConnected) { nodeList = nodeList.concat(graph.getNodes()); } } return nodeList; }; CoSELayout.prototype.createBendpoints = function () { var edges = []; edges = edges.concat(this.graphManager.getAllEdges()); var visited = new Set(); var i; for (i = 0; i < edges.length; i++) { var edge = edges[i]; if (!visited.has(edge)) { var source = edge.getSource(); var target = edge.getTarget(); if (source == target) { edge.getBendpoints().push(new PointD()); edge.getBendpoints().push(new PointD()); this.createDummyNodesForBendpoints(edge); visited.add(edge); } else { var edgeList = []; edgeList = edgeList.concat(source.getEdgeListToNode(target)); edgeList = edgeList.concat(target.getEdgeListToNode(source)); if (!visited.has(edgeList[0])) { if (edgeList.length > 1) { var k; for (k = 0; k < edgeList.length; k++) { var multiEdge = edgeList[k]; multiEdge.getBendpoints().push(new PointD()); this.createDummyNodesForBendpoints(multiEdge); } } edgeList.forEach(function(edge){ visited.add(edge); }); } } } if (visited.size == edges.length) { break; } } }; CoSELayout.prototype.positionNodesRadially = function (forest) { // We tile the trees to a grid row by row; first tree starts at (0,0) var currentStartingPoint = new Point(0, 0); var numberOfColumns = Math.ceil(Math.sqrt(forest.length)); var height = 0; var currentY = 0; var currentX = 0; var point = new PointD(0, 0); for (var i = 0; i < forest.length; i++) { if (i % numberOfColumns == 0) { // Start of a new row, make the x coordinate 0, increment the // y coordinate with the max height of the previous row currentX = 0; currentY = height; if (i != 0) { currentY += CoSEConstants.DEFAULT_COMPONENT_SEPERATION; } height = 0; } var tree = forest[i]; // Find the center of the tree var centerNode = Layout.findCenterOfTree(tree); // Set the staring point of the next tree currentStartingPoint.x = currentX; currentStartingPoint.y = currentY; // Do a radial layout starting with the center point = CoSELayout.radialLayout(tree, centerNode, currentStartingPoint); if (point.y > height) { height = Math.floor(point.y); } currentX = Math.floor(point.x + CoSEConstants.DEFAULT_COMPONENT_SEPERATION); } this.transform( new PointD(LayoutConstants.WORLD_CENTER_X - point.x / 2, LayoutConstants.WORLD_CENTER_Y - point.y / 2)); }; CoSELayout.radialLayout = function (tree, centerNode, startingPoint) { var radialSep = Math.max(this.maxDiagonalInTree(tree), CoSEConstants.DEFAULT_RADIAL_SEPARATION); CoSELayout.branchRadialLayout(centerNode, null, 0, 359, 0, radialSep); var bounds = LGraph.calculateBounds(tree); var transform = new Transform(); transform.setDeviceOrgX(bounds.getMinX()); transform.setDeviceOrgY(bounds.getMinY()); transform.setWorldOrgX(startingPoint.x); transform.setWorldOrgY(startingPoint.y); for (var i = 0; i < tree.length; i++) { var node = tree[i]; node.transform(transform); } var bottomRight = new PointD(bounds.getMaxX(), bounds.getMaxY()); return transform.inverseTransformPoint(bottomRight); }; CoSELayout.branchRadialLayout = function (node, parentOfNode, startAngle, endAngle, distance, radialSeparation) { // First, position this node by finding its angle. var halfInterval = ((endAngle - startAngle) + 1) / 2; if (halfInterval < 0) { halfInterval += 180; } var nodeAngle = (halfInterval + startAngle) % 360; var teta = (nodeAngle * IGeometry.TWO_PI) / 360; // Make polar to java cordinate conversion. var cos_teta = Math.cos(teta); var x_ = distance * Math.cos(teta); var y_ = distance * Math.sin(teta); node.setCenter(x_, y_); // Traverse all neighbors of this node and recursively call this // function. var neighborEdges = []; neighborEdges = neighborEdges.concat(node.getEdges()); var childCount = neighborEdges.length; if (parentOfNode != null) { childCount--; } var branchCount = 0; var incEdgesCount = neighborEdges.length; var startIndex; var edges = node.getEdgesBetween(parentOfNode); // If there are multiple edges, prune them until there remains only one // edge. while (edges.length > 1) { //neighborEdges.remove(edges.remove(0)); var temp = edges[0]; edges.splice(0, 1); var index = neighborEdges.indexOf(temp); if (index >= 0) { neighborEdges.splice(index, 1); } incEdgesCount--; childCount--; } if (parentOfNode != null) { //assert edges.length == 1; startIndex = (neighborEdges.indexOf(edges[0]) + 1) % incEdgesCount; } else { startIndex = 0; } var stepAngle = Math.abs(endAngle - startAngle) / childCount; for (var i = startIndex; branchCount != childCount; i = (++i) % incEdgesCount) { var currentNeighbor = neighborEdges[i].getOtherEnd(node); // Don't back traverse to root node in current tree. if (currentNeighbor == parentOfNode) { continue; } var childStartAngle = (startAngle + branchCount * stepAngle) % 360; var childEndAngle = (childStartAngle + stepAngle) % 360; CoSELayout.branchRadialLayout(currentNeighbor, node, childStartAngle, childEndAngle, distance + radialSeparation, radialSeparation); branchCount++; } }; CoSELayout.maxDiagonalInTree = function (tree) { var maxDiagonal = Integer.MIN_VALUE; for (var i = 0; i < tree.length; i++) { var node = tree[i]; var diagonal = node.getDiagonal(); if (diagonal > maxDiagonal) { maxDiagonal = diagonal; } } return maxDiagonal; }; CoSELayout.prototype.calcRepulsionRange = function () { // formula is 2 x (level + 1) x idealEdgeLength return (2 * (this.level + 1) * this.idealEdgeLength); }; // Tiling methods // Group zero degree members whose parents are not to be tiled, create dummy parents where needed and fill memberGroups by their dummp parent id's CoSELayout.prototype.groupZeroDegreeMembers = function () { var self = this; // array of [parent_id x oneDegreeNode_id] var tempMemberGroups = {}; // A temporary map of parent node and its zero degree members this.memberGroups = {}; // A map of dummy parent node and its zero degree members whose parents are not to be tiled this.idToDummyNode = {}; // A map of id to dummy node var zeroDegree = []; // List of zero degree nodes whose parents are not to be tiled var allNodes = this.graphManager.getAllNodes(); // Fill zero degree list for (var i = 0; i < allNodes.length; i++) { var node = allNodes[i]; var parent = node.getParent(); // If a node has zero degree and its parent is not to be tiled if exists add that node to zeroDegres list if (this.getNodeDegreeWithChildren(node) === 0 && ( parent.id == undefined || !this.getToBeTiled(parent) ) ) { zeroDegree.push(node); } } // Create a map of parent node and its zero degree members for (var i = 0; i < zeroDegree.length; i++) { var node = zeroDegree[i]; // Zero degree node itself var p_id = node.getParent().id; // Parent id if (typeof tempMemberGroups[p_id] === "undefined") tempMemberGroups[p_id] = []; tempMemberGroups[p_id] = tempMemberGroups[p_id].concat(node); // Push node to the list belongs to its parent in tempMemberGroups } // If there are at least two nodes at a level, create a dummy compound for them Object.keys(tempMemberGroups).forEach(function(p_id) { if (tempMemberGroups[p_id].length > 1) { var dummyCompoundId = "DummyCompound_" + p_id; // The id of dummy compound which will be created soon self.memberGroups[dummyCompoundId] = tempMemberGroups[p_id]; // Add dummy compound to memberGroups var parent = tempMemberGroups[p_id][0].getParent(); // The parent of zero degree nodes will be the parent of new dummy compound // Create a dummy compound with calculated id var dummyCompound = new CoSENode(self.graphManager); dummyCompound.id = dummyCompoundId; dummyCompound.paddingLeft = parent.paddingLeft || 0; dummyCompound.paddingRight = parent.paddingRight || 0; dummyCompound.paddingBottom = parent.paddingBottom || 0; dummyCompound.paddingTop = parent.paddingTop || 0; self.idToDummyNode[dummyCompoundId] = dummyCompound; var dummyParentGraph = self.getGraphManager().add(self.newGraph(), dummyCompound); var parentGraph = parent.getChild(); // Add dummy compound to parent the graph parentGraph.add(dummyCompound); // For each zero degree node in this level remove it from its parent graph and add it to the graph of dummy parent for (var i = 0; i < tempMemberGroups[p_id].length; i++) { var node = tempMemberGroups[p_id][i]; parentGraph.remove(node); dummyParentGraph.add(node); } } }); }; CoSELayout.prototype.clearCompounds = function () { var childGraphMap = {}; var idToNode = {}; // Get compound ordering by finding the inner one first this.performDFSOnCompounds(); for (var i = 0; i < this.compoundOrder.length; i++) { idToNode[this.compoundOrder[i].id] = this.compoundOrder[i]; childGraphMap[this.compoundOrder[i].id] = [].concat(this.compoundOrder[i].getChild().getNodes()); // Remove children of compounds this.graphManager.remove(this.compoundOrder[i].getChild()); this.compoundOrder[i].child = null; } this.graphManager.resetAllNodes(); // Tile the removed children this.tileCompoundMembers(childGraphMap, idToNode); }; CoSELayout.prototype.clearZeroDegreeMembers = function () { var self = this; var tiledZeroDegreePack = this.tiledZeroDegreePack = []; Object.keys(this.memberGroups).forEach(function(id) { var compoundNode = self.idToDummyNode[id]; // Get the dummy compound tiledZeroDegreePack[id] = self.tileNodes(self.memberGroups[id], compoundNode.paddingLeft + compoundNode.paddingRight); // Set the width and height of the dummy compound as calculated compoundNode.rect.width = tiledZeroDegreePack[id].width; compoundNode.rect.height = tiledZeroDegreePack[id].height; compoundNode.setCenter(tiledZeroDegreePack[id].centerX, tiledZeroDegreePack[id].centerY); // compound left and top margings for labels // when node labels are included, these values may be set to different values below and are used in tilingPostLayout, // otherwise they stay as zero compoundNode.labelMarginLeft = 0; compoundNode.labelMarginTop = 0; // Update compound bounds considering its label properties and set label margins for left and top if(CoSEConstants.NODE_DIMENSIONS_INCLUDE_LABELS){ var width = compoundNode.rect.width; var height = compoundNode.rect.height; if(compoundNode.labelWidth){ if(compoundNode.labelPosHorizontal == "left"){ compoundNode.rect.x -= (compoundNode.labelWidth); compoundNode.setWidth(width + compoundNode.labelWidth); compoundNode.labelMarginLeft = compoundNode.labelWidth; } else if(compoundNode.labelPosHorizontal == "center" && compoundNode.labelWidth > width){ compoundNode.rect.x -= (compoundNode.labelWidth - width) / 2; compoundNode.setWidth(compoundNode.labelWidth); compoundNode.labelMarginLeft = (compoundNode.labelWidth - width) / 2; } else if(compoundNode.labelPosHorizontal == "right"){ compoundNode.setWidth(width + compoundNode.labelWidth); } } if(compoundNode.labelHeight){ if(compoundNode.labelPosVertical == "top"){ compoundNode.rect.y -= (compoundNode.labelHeight); compoundNode.setHeight(height + compoundNode.labelHeight); compoundNode.labelMarginTop = compoundNode.labelHeight; } else if(compoundNode.labelPosVertical == "center" && compoundNode.labelHeight > height){ compoundNode.rect.y -= (compoundNode.labelHeight - height) / 2; compoundNode.setHeight(compoundNode.labelHeight); compoundNode.labelMarginTop = (compoundNode.labelHeight - height) / 2; } else if(compoundNode.labelPosVertical == "bottom"){ compoundNode.setHeight(height + compoundNode.labelHeight); } } } }); }; CoSELayout.prototype.repopulateCompounds = function () { for (var i = this.compoundOrder.length - 1; i >= 0; i--) { var lCompoundNode = this.compoundOrder[i]; var id = lCompoundNode.id; var horizontalMargin = lCompoundNode.paddingLeft; var verticalMargin = lCompoundNode.paddingTop; var labelMarginLeft = lCompoundNode.labelMarginLeft; var labelMarginTop = lCompoundNode.labelMarginTop; this.adjustLocations(this.tiledMemberPack[id], lCompoundNode.rect.x, lCompoundNode.rect.y, horizontalMargin, verticalMargin, labelMarginLeft, labelMarginTop); } }; CoSELayout.prototype.repopulateZeroDegreeMembers = function () { var self = this; var tiledPack = this.tiledZeroDegreePack; Object.keys(tiledPack).forEach(function(id) { var compoundNode = self.idToDummyNode[id]; // Get the dummy compound by its id var horizontalMargin = compoundNode.paddingLeft; var verticalMargin = compoundNode.paddingTop; var labelMarginLeft = compoundNode.labelMarginLeft; var labelMarginTop = compoundNode.labelMarginTop; // Adjust the positions of nodes wrt its compound self.adjustLocations(tiledPack[id], compoundNode.rect.x, compoundNode.rect.y, horizontalMargin, verticalMargin, labelMarginLeft, labelMarginTop); }); }; CoSELayout.prototype.getToBeTiled = function (node) { var id = node.id; //firstly check the previous results if (this.toBeTiled[id] != null) { return this.toBeTiled[id]; } //only compound nodes are to be tiled var childGraph = node.getChild(); if (childGraph == null) { this.toBeTiled[id] = false; return false; } var children = childGraph.getNodes(); // Get the children nodes //a compound node is not to be tiled if all of its compound children are not to be tiled for (var i = 0; i < children.length; i++) { var theChild = children[i]; if (this.getNodeDegree(theChild) > 0) { this.toBeTiled[id] = false; return false; } //pass the children not having the compound structure if (theChild.getChild() == null) { this.toBeTiled[theChild.id] = false; continue; } if (!this.getToBeTiled(theChild)) { this.toBeTiled[id] = false; return false; } } this.toBeTiled[id] = true; return true; }; // Get degree of a node depending of its edges and independent of its children CoSELayout.prototype.getNodeDegree = function (node) { var id = node.id; var edges = node.getEdges(); var degree = 0; // For the edges connected for (var i = 0; i < edges.length; i++) { var edge = edges[i]; if (edge.getSource().id !== edge.getTarget().id) { degree = degree + 1; } } return degree; }; // Get degree of a node with its children CoSELayout.prototype.getNodeDegreeWithChildren = function (node) { var degree = this.getNodeDegree(node); if (node.getChild() == null) { return degree; } var children = node.getChild().getNodes(); for (var i = 0; i < children.length; i++) { var child = children[i]; degree += this.getNodeDegreeWithChildren(child); } return degree; }; CoSELayout.prototype.performDFSOnCompounds = function () { this.compoundOrder = []; this.fillCompexOrderByDFS(this.graphManager.getRoot().getNodes()); }; CoSELayout.prototype.fillCompexOrderByDFS = function (children) { for (var i = 0; i < children.length; i++) { var child = children[i]; if (child.getChild() != null) { this.fillCompexOrderByDFS(child.getChild().getNodes()); } if (this.getToBeTiled(child)) { this.compoundOrder.push(child); } } }; /** * This method places each zero degree member wrt given (x,y) coordinates (top left). */ CoSELayout.prototype.adjustLocations = function (organization, x, y, compoundHorizontalMargin, compoundVerticalMargin, compoundLabelMarginLeft, compoundLabelMarginTop) { x += compoundHorizontalMargin + compoundLabelMarginLeft; y += compoundVerticalMargin + compoundLabelMarginTop; var left = x; for (var i = 0; i < organization.rows.length; i++) { var row = organization.rows[i]; x = left; var maxHeight = 0; for (var j = 0; j < row.length; j++) { var lnode = row[j]; lnode.rect.x = x;// + lnode.rect.width / 2; lnode.rect.y = y;// + lnode.rect.height / 2; x += lnode.rect.width + organization.horizontalPadding; if (lnode.rect.height > maxHeight) maxHeight = lnode.rect.height; } y += maxHeight + organization.verticalPadding; } }; CoSELayout.prototype.tileCompoundMembers = function (childGraphMap, idToNode) { var self = this; this.tiledMemberPack = []; Object.keys(childGraphMap).forEach(function(id) { // Get the compound node var compoundNode = idToNode[id]; self.tiledMemberPack[id] = self.tileNodes(childGraphMap[id], compoundNode.paddingLeft + compoundNode.paddingRight); compoundNode.rect.width = self.tiledMemberPack[id].width; compoundNode.rect.height = self.tiledMemberPack[id].height; compoundNode.setCenter(self.tiledMemberPack[id].centerX, self.tiledMemberPack[id].centerY); // compound left and top margings for labels // when node labels are included, these values may be set to different values below and are used in tilingPostLayout, // otherwise they stay as zero compoundNode.labelMarginLeft = 0; compoundNode.labelMarginTop = 0; // Update compound bounds considering its label properties and set label margins for left and top if(CoSEConstants.NODE_DIMENSIONS_INCLUDE_LABELS){ var width = compoundNode.rect.width; var height = compoundNode.rect.height; if(compoundNode.labelWidth){ if(compoundNode.labelPosH