UNPKG

cose-base

Version:

Core module for compound spring embedder based layout styles

848 lines (768 loc) 33.6 kB
var CoSEConstants = require('./CoSEConstants'); var LinkedList = require('layout-base').LinkedList; var Matrix = require('layout-base').Matrix; var SVD = require('layout-base').SVD; function ConstraintHandler() { } ConstraintHandler.handleConstraints = function (layout) { // let layout = this.graphManager.getLayout(); // get constraints from layout let constraints = {}; constraints.fixedNodeConstraint = layout.constraints.fixedNodeConstraint; constraints.alignmentConstraint = layout.constraints.alignmentConstraint; constraints.relativePlacementConstraint = layout.constraints.relativePlacementConstraint; let idToNodeMap = new Map(); let nodeIndexes = new Map(); let xCoords = []; let yCoords = []; let allNodes = layout.getAllNodes(); let index = 0; // fill index map and coordinates for (let i = 0; i < allNodes.length; i++) { let node = allNodes[i]; if (node.getChild() == null) { nodeIndexes.set(node.id, index++); xCoords.push(node.getCenterX()); yCoords.push(node.getCenterY()); idToNodeMap.set(node.id, node); } } // if there exists relative placement constraint without gap value, set it to default if (constraints.relativePlacementConstraint) { constraints.relativePlacementConstraint.forEach(function(constraint) { if (!constraint.gap && constraint.gap != 0) { if (constraint.left) { constraint.gap = CoSEConstants.DEFAULT_EDGE_LENGTH + idToNodeMap.get(constraint.left).getWidth()/2 + idToNodeMap.get(constraint.right).getWidth()/2; } else { constraint.gap = CoSEConstants.DEFAULT_EDGE_LENGTH + idToNodeMap.get(constraint.top).getHeight()/2 + idToNodeMap.get(constraint.bottom).getHeight()/2; } } }); } /* auxiliary functions */ // calculate difference between two position objects let calculatePositionDiff = function(pos1, pos2) { return {x: pos1.x - pos2.x, y: pos1.y - pos2.y}; }; // calculate average position of the nodes let calculateAvgPosition = function(nodeIdSet) { let xPosSum = 0; let yPosSum = 0; nodeIdSet.forEach(function(nodeId) { xPosSum += xCoords[nodeIndexes.get(nodeId)]; yPosSum += yCoords[nodeIndexes.get(nodeId)]; }); return {x: xPosSum / nodeIdSet.size, y: yPosSum / nodeIdSet.size}; }; // find an appropriate positioning for the nodes in a given graph according to relative placement constraints // this function also takes the fixed nodes and alignment constraints into account // graph: dag to be evaluated, direction: "horizontal" or "vertical", // fixedNodes: set of fixed nodes to consider during evaluation, dummyPositions: appropriate coordinates of the dummy nodes let findAppropriatePositionForRelativePlacement = function(graph, direction, fixedNodes, dummyPositions, componentSources) { // find union of two sets function setUnion(setA, setB) { let union = new Set(setA); for (let elem of setB) { union.add(elem); } return union; } // find indegree count for each node let inDegrees = new Map(); graph.forEach(function(value, key) { inDegrees.set(key, 0); }); graph.forEach(function(value, key) { value.forEach(function(adjacent) { inDegrees.set(adjacent.id, inDegrees.get(adjacent.id) + 1); }); }); let positionMap = new Map(); // keeps the position for each node let pastMap = new Map(); // keeps the predecessors(past) of a node let queue = new LinkedList(); inDegrees.forEach(function(value, key) { if (value == 0) { queue.push(key); if (!fixedNodes) { if (direction == "horizontal") { positionMap.set(key, nodeIndexes.has(key) ? xCoords[nodeIndexes.get(key)] : dummyPositions.get(key)); } else { positionMap.set(key, nodeIndexes.has(key) ? yCoords[nodeIndexes.get(key)] : dummyPositions.get(key)); } } } else { positionMap.set(key, Number.NEGATIVE_INFINITY); } if (fixedNodes) { pastMap.set(key, new Set([key])); } }); // align sources of each component in enforcement phase if (fixedNodes) { componentSources.forEach(function(component) { let fixedIds = []; component.forEach(function(nodeId) { if (fixedNodes.has(nodeId)) { fixedIds.push(nodeId); } }); if (fixedIds.length > 0) { let position = 0; fixedIds.forEach(function(fixedId) { if (direction == "horizontal") { positionMap.set(fixedId, nodeIndexes.has(fixedId) ? xCoords[nodeIndexes.get(fixedId)] : dummyPositions.get(fixedId)); position += positionMap.get(fixedId); } else { positionMap.set(fixedId, nodeIndexes.has(fixedId) ? yCoords[nodeIndexes.get(fixedId)] : dummyPositions.get(fixedId)); position += positionMap.get(fixedId); } }); position = position / fixedIds.length; component.forEach(function(nodeId) { if (!fixedNodes.has(nodeId)) { positionMap.set(nodeId, position); } }); } else { let position = 0; component.forEach(function(nodeId) { if (direction == "horizontal") { position += nodeIndexes.has(nodeId) ? xCoords[nodeIndexes.get(nodeId)] : dummyPositions.get(nodeId); } else { position += nodeIndexes.has(nodeId) ? yCoords[nodeIndexes.get(nodeId)] : dummyPositions.get(nodeId); } }); position = position / component.length; component.forEach(function(nodeId) { positionMap.set(nodeId, position); }); } }); } // calculate positions of the nodes while (queue.length != 0) { let currentNode = queue.shift(); let neighbors = graph.get(currentNode); neighbors.forEach(function(neighbor) { if (positionMap.get(neighbor.id) < (positionMap.get(currentNode) + neighbor.gap)) { if (fixedNodes && fixedNodes.has(neighbor.id)) { let fixedPosition; if (direction == "horizontal") { fixedPosition = nodeIndexes.has(neighbor.id) ? xCoords[nodeIndexes.get(neighbor.id)] : dummyPositions.get(neighbor.id); } else { fixedPosition = nodeIndexes.has(neighbor.id) ? yCoords[nodeIndexes.get(neighbor.id)] : dummyPositions.get(neighbor.id); } positionMap.set(neighbor.id, fixedPosition); // TODO: may do unnecessary work if (fixedPosition < (positionMap.get(currentNode) + neighbor.gap)) { let diff = (positionMap.get(currentNode) + neighbor.gap) - fixedPosition; pastMap.get(currentNode).forEach(function(nodeId) { positionMap.set(nodeId, positionMap.get(nodeId) - diff); }); } } else { positionMap.set(neighbor.id, positionMap.get(currentNode) + neighbor.gap); } } inDegrees.set(neighbor.id, inDegrees.get(neighbor.id) - 1); if (inDegrees.get(neighbor.id) == 0) { queue.push(neighbor.id); } if (fixedNodes) { pastMap.set(neighbor.id, setUnion(pastMap.get(currentNode), pastMap.get(neighbor.id))); } }); } // readjust position of the nodes after enforcement if (fixedNodes) { // find indegree count for each node let sinkNodes = new Set(); graph.forEach(function(value, key) { if (value.length == 0) { sinkNodes.add(key); } }); let components = []; pastMap.forEach(function(value, key) { if (sinkNodes.has(key)) { let isFixedComponent = false; for (let nodeId of value) { if (fixedNodes.has(nodeId)) { isFixedComponent = true; } } if (!isFixedComponent) { let isExist = false; let existAt; components.forEach(function(component, index) { if (component.has([...value][0])) { isExist = true; existAt = index; } }); if (!isExist) { components.push(new Set(value)); } else { value.forEach(function(ele) { components[existAt].add(ele); }); } } } }); components.forEach(function(component, index) { let minBefore = Number.POSITIVE_INFINITY; let minAfter = Number.POSITIVE_INFINITY; let maxBefore = Number.NEGATIVE_INFINITY; let maxAfter = Number.NEGATIVE_INFINITY; for (let nodeId of component) { let posBefore; if (direction == "horizontal") { posBefore = nodeIndexes.has(nodeId) ? xCoords[nodeIndexes.get(nodeId)] : dummyPositions.get(nodeId); } else { posBefore = nodeIndexes.has(nodeId) ? yCoords[nodeIndexes.get(nodeId)] : dummyPositions.get(nodeId); } let posAfter = positionMap.get(nodeId); if (posBefore < minBefore) { minBefore = posBefore; } if (posBefore > maxBefore) { maxBefore = posBefore; } if (posAfter < minAfter) { minAfter = posAfter; } if (posAfter > maxAfter) { maxAfter = posAfter; } } let diff = (minBefore + maxBefore) / 2 - (minAfter + maxAfter) / 2; for (let nodeId of component) { positionMap.set(nodeId, positionMap.get(nodeId) + diff); } }); } return positionMap; }; // find transformation based on rel. placement constraints if there are both alignment and rel. placement constraints // or if there are only rel. placement contraints where the largest component isn't sufficiently large let applyReflectionForRelativePlacement = function (relativePlacementConstraints) { // variables to count votes let reflectOnY = 0, notReflectOnY = 0; let reflectOnX = 0, notReflectOnX = 0; relativePlacementConstraints.forEach(function(constraint) { if (constraint.left) { (xCoords[nodeIndexes.get(constraint.left)] - xCoords[nodeIndexes.get(constraint.right)] >= 0) ? reflectOnY++ : notReflectOnY++; } else { (yCoords[nodeIndexes.get(constraint.top)] - yCoords[nodeIndexes.get(constraint.bottom)] >= 0) ? reflectOnX++ : notReflectOnX++; } }); if (reflectOnY > notReflectOnY && reflectOnX > notReflectOnX) { for (let i = 0; i < nodeIndexes.size; i++) { xCoords[i] = -1 * xCoords[i]; yCoords[i] = -1 * yCoords[i]; } } else if (reflectOnY > notReflectOnY) { for (let i = 0; i < nodeIndexes.size; i++) { xCoords[i] = -1 * xCoords[i]; } } else if (reflectOnX > notReflectOnX) { for (let i = 0; i < nodeIndexes.size; i++) { yCoords[i] = -1 * yCoords[i]; } } }; // find weakly connected components in undirected graph let findComponents = function(graph) { // find weakly connected components in dag let components = []; let queue = new LinkedList(); let visited = new Set(); let count = 0; graph.forEach(function(value, key) { if (!visited.has(key)) { components[count] = []; let currentNode = key; queue.push(currentNode); visited.add(currentNode); components[count].push(currentNode); while (queue.length != 0) { currentNode = queue.shift(); let neighbors = graph.get(currentNode); neighbors.forEach(function(neighbor) { if (!visited.has(neighbor.id)) { queue.push(neighbor.id); visited.add(neighbor.id); components[count].push(neighbor.id); } }); } count++; } }); return components; }; // return undirected version of given dag let dagToUndirected = function(dag) { let undirected = new Map(); dag.forEach(function(value, key) { undirected.set(key, []); }); dag.forEach(function(value, key) { value.forEach(function(adjacent) { undirected.get(key).push(adjacent); undirected.get(adjacent.id).push({id: key, gap: adjacent.gap, direction: adjacent.direction}); }); }); return undirected; }; // return reversed (directions inverted) version of given dag let dagToReversed = function(dag) { let reversed = new Map(); dag.forEach(function(value, key) { reversed.set(key, []); }); dag.forEach(function(value, key) { value.forEach(function(adjacent) { reversed.get(adjacent.id).push({id: key, gap: adjacent.gap, direction: adjacent.direction}); }); }); return reversed; }; /**** apply transformation to the initial draft layout to better align with constrained nodes ****/ // solve the Orthogonal Procrustean Problem to rotate and/or reflect initial draft layout // here we follow the solution in Chapter 20.2 of Borg, I. & Groenen, P. (2005) Modern Multidimensional Scaling: Theory and Applications /* construct source and target configurations */ let targetMatrix = []; // A - target configuration let sourceMatrix = []; // B - source configuration let standardTransformation = false; // false for no transformation, true for standart (Procrustes) transformation (rotation and/or reflection) let reflectionType = false; // false/true for reflection check, 'reflectOnX', 'reflectOnY' or 'reflectOnBoth' for reflection type if necessary let fixedNodes = new Set(); let dag = new Map(); // adjacency list to keep directed acyclic graph (dag) that consists of relative placement constraints let dagUndirected = new Map(); // undirected version of the dag let components = []; // weakly connected components // fill fixedNodes collection to use later if (constraints.fixedNodeConstraint) { constraints.fixedNodeConstraint.forEach(function(nodeData) { fixedNodes.add(nodeData.nodeId); }); } // construct dag from relative placement constraints if (constraints.relativePlacementConstraint) { // construct both directed and undirected version of the dag constraints.relativePlacementConstraint.forEach(function(constraint) { if (constraint.left) { if (dag.has(constraint.left)) { dag.get(constraint.left).push({id: constraint.right, gap: constraint.gap, direction: "horizontal"}); } else { dag.set(constraint.left, [{id: constraint.right, gap: constraint.gap, direction: "horizontal"}]); } if (!dag.has(constraint.right)) { dag.set(constraint.right, []); } } else { if (dag.has(constraint.top)) { dag.get(constraint.top).push({id: constraint.bottom, gap: constraint.gap, direction: "vertical"}); } else { dag.set(constraint.top, [{id: constraint.bottom, gap: constraint.gap, direction: "vertical"}]); } if (!dag.has(constraint.bottom)) { dag.set(constraint.bottom, []); } } }); dagUndirected = dagToUndirected(dag); components = findComponents(dagUndirected); } if (CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING) { // first check fixed node constraint if (constraints.fixedNodeConstraint && constraints.fixedNodeConstraint.length > 1) { constraints.fixedNodeConstraint.forEach(function(nodeData, i) { targetMatrix[i] = [nodeData.position.x, nodeData.position.y]; sourceMatrix[i] = [xCoords[nodeIndexes.get(nodeData.nodeId)], yCoords[nodeIndexes.get(nodeData.nodeId)]]; }); standardTransformation = true; } else if (constraints.alignmentConstraint) { // then check alignment constraint let count = 0; if (constraints.alignmentConstraint.vertical) { let verticalAlign = constraints.alignmentConstraint.vertical; for (let i = 0; i < verticalAlign.length; i++) { let alignmentSet = new Set(); verticalAlign[i].forEach(function(nodeId) { alignmentSet.add(nodeId); }); let intersection = new Set([...alignmentSet].filter(x => fixedNodes.has(x))); let xPos; if (intersection.size > 0) xPos = xCoords[nodeIndexes.get(intersection.values().next().value)]; else xPos = calculateAvgPosition(alignmentSet).x; verticalAlign[i].forEach(function(nodeId) { targetMatrix[count] = [xPos, yCoords[nodeIndexes.get(nodeId)]]; sourceMatrix[count] = [xCoords[nodeIndexes.get(nodeId)], yCoords[nodeIndexes.get(nodeId)]]; count++; }); } standardTransformation = true; } if (constraints.alignmentConstraint.horizontal) { let horizontalAlign = constraints.alignmentConstraint.horizontal; for (let i = 0; i < horizontalAlign.length; i++) { let alignmentSet = new Set(); horizontalAlign[i].forEach(function(nodeId) { alignmentSet.add(nodeId); }); let intersection = new Set([...alignmentSet].filter(x => fixedNodes.has(x))); let yPos; if (intersection.size > 0) yPos = xCoords[nodeIndexes.get(intersection.values().next().value)]; else yPos = calculateAvgPosition(alignmentSet).y; horizontalAlign[i].forEach(function(nodeId) { targetMatrix[count] = [xCoords[nodeIndexes.get(nodeId)], yPos]; sourceMatrix[count] = [xCoords[nodeIndexes.get(nodeId)], yCoords[nodeIndexes.get(nodeId)]]; count++; }); } standardTransformation = true; } if (constraints.relativePlacementConstraint) { reflectionType = true; } } else if (constraints.relativePlacementConstraint) { // finally check relative placement constraint // find largest component in dag let largestComponentSize = 0; let largestComponentIndex = 0; for (let i = 0; i < components.length; i++) { if (components[i].length > largestComponentSize) { largestComponentSize = components[i].length; largestComponentIndex = i; } } // if largest component isn't dominant, then take the votes for reflection if (largestComponentSize < (dagUndirected.size / 2)) { applyReflectionForRelativePlacement(constraints.relativePlacementConstraint); standardTransformation = false; reflectionType = false; } else { // use largest component for transformation // construct horizontal and vertical subgraphs in the largest component let subGraphOnHorizontal = new Map(); let subGraphOnVertical = new Map(); let constraintsInlargestComponent = []; components[largestComponentIndex].forEach(function(nodeId) { dag.get(nodeId).forEach(function(adjacent) { if (adjacent.direction == "horizontal") { if (subGraphOnHorizontal.has(nodeId)) { subGraphOnHorizontal.get(nodeId).push(adjacent); } else { subGraphOnHorizontal.set(nodeId, [adjacent]); } if (!subGraphOnHorizontal.has(adjacent.id)) { subGraphOnHorizontal.set(adjacent.id, []); } constraintsInlargestComponent.push({left: nodeId, right: adjacent.id}); } else { if (subGraphOnVertical.has(nodeId)) { subGraphOnVertical.get(nodeId).push(adjacent); } else { subGraphOnVertical.set(nodeId, [adjacent]); } if (!subGraphOnVertical.has(adjacent.id)) { subGraphOnVertical.set(adjacent.id, []); } constraintsInlargestComponent.push({top: nodeId, bottom: adjacent.id}); } }); }); applyReflectionForRelativePlacement(constraintsInlargestComponent); reflectionType = false; // calculate appropriate positioning for subgraphs let positionMapHorizontal = findAppropriatePositionForRelativePlacement(subGraphOnHorizontal, "horizontal"); let positionMapVertical = findAppropriatePositionForRelativePlacement(subGraphOnVertical, "vertical"); // construct source and target configuration components[largestComponentIndex].forEach(function(nodeId, i) { sourceMatrix[i] = [xCoords[nodeIndexes.get(nodeId)], yCoords[nodeIndexes.get(nodeId)]]; targetMatrix[i] = []; if (positionMapHorizontal.has(nodeId)) { targetMatrix[i][0] = positionMapHorizontal.get(nodeId); } else { targetMatrix[i][0] = xCoords[nodeIndexes.get(nodeId)]; } if (positionMapVertical.has(nodeId)) { targetMatrix[i][1] = positionMapVertical.get(nodeId); } else { targetMatrix[i][1] = yCoords[nodeIndexes.get(nodeId)]; } }); standardTransformation = true; } } // if transformation is required, then calculate and apply transformation matrix if (standardTransformation) { /* calculate transformation matrix */ let transformationMatrix; let targetMatrixTranspose = Matrix.transpose(targetMatrix); // A' let sourceMatrixTranspose = Matrix.transpose(sourceMatrix); // B' // centralize transpose matrices for (let i = 0; i < targetMatrixTranspose.length; i++) { targetMatrixTranspose[i] = Matrix.multGamma(targetMatrixTranspose[i]); sourceMatrixTranspose[i] = Matrix.multGamma(sourceMatrixTranspose[i]); } // do actual calculation for transformation matrix let tempMatrix = Matrix.multMat(targetMatrixTranspose, Matrix.transpose(sourceMatrixTranspose)); // tempMatrix = A'B let SVDResult = SVD.svd(tempMatrix); // SVD(A'B) = USV', svd function returns U, S and V transformationMatrix = Matrix.multMat(SVDResult.V, Matrix.transpose(SVDResult.U)); // transformationMatrix = T = VU' /* apply found transformation matrix to obtain final draft layout */ for (let i = 0; i < nodeIndexes.size; i++) { let temp1 = [xCoords[i], yCoords[i]]; let temp2 = [transformationMatrix[0][0], transformationMatrix[1][0]]; let temp3 = [transformationMatrix[0][1], transformationMatrix[1][1]]; xCoords[i] = Matrix.dotProduct(temp1, temp2); yCoords[i] = Matrix.dotProduct(temp1, temp3); } // applied only both alignment and rel. placement constraints exist if (reflectionType) { applyReflectionForRelativePlacement(constraints.relativePlacementConstraint); } } } if (CoSEConstants.ENFORCE_CONSTRAINTS) { /**** enforce constraints on the transformed draft layout ****/ /* first enforce fixed node constraint */ if (constraints.fixedNodeConstraint && constraints.fixedNodeConstraint.length > 0) { let translationAmount = { x: 0, y: 0 }; constraints.fixedNodeConstraint.forEach(function(nodeData, i) { let posInTheory = {x: xCoords[nodeIndexes.get(nodeData.nodeId)], y: yCoords[nodeIndexes.get(nodeData.nodeId)]}; let posDesired = nodeData.position; let posDiff = calculatePositionDiff(posDesired, posInTheory); translationAmount.x += posDiff.x; translationAmount.y += posDiff.y; }); translationAmount.x /= constraints.fixedNodeConstraint.length; translationAmount.y /= constraints.fixedNodeConstraint.length; xCoords.forEach(function(value, i) { xCoords[i] += translationAmount.x; }); yCoords.forEach(function(value, i) { yCoords[i] += translationAmount.y; }); constraints.fixedNodeConstraint.forEach(function(nodeData) { xCoords[nodeIndexes.get(nodeData.nodeId)] = nodeData.position.x; yCoords[nodeIndexes.get(nodeData.nodeId)] = nodeData.position.y; }); } /* then enforce alignment constraint */ if (constraints.alignmentConstraint) { if (constraints.alignmentConstraint.vertical) { let xAlign = constraints.alignmentConstraint.vertical; for (let i = 0; i < xAlign.length; i++) { let alignmentSet = new Set(); xAlign[i].forEach(function(nodeId) { alignmentSet.add(nodeId); }); let intersection = new Set([...alignmentSet].filter(x => fixedNodes.has(x))); let xPos; if (intersection.size > 0) xPos = xCoords[nodeIndexes.get(intersection.values().next().value)]; else xPos = calculateAvgPosition(alignmentSet).x; alignmentSet.forEach(function(nodeId) { if (!fixedNodes.has(nodeId)) xCoords[nodeIndexes.get(nodeId)] = xPos; }); } } if (constraints.alignmentConstraint.horizontal) { let yAlign = constraints.alignmentConstraint.horizontal; for (let i = 0; i < yAlign.length; i++) { let alignmentSet = new Set(); yAlign[i].forEach(function(nodeId) { alignmentSet.add(nodeId); }); let intersection = new Set([...alignmentSet].filter(x => fixedNodes.has(x))); let yPos; if (intersection.size > 0) yPos = yCoords[nodeIndexes.get(intersection.values().next().value)]; else yPos = calculateAvgPosition(alignmentSet).y; alignmentSet.forEach(function(nodeId) { if (!fixedNodes.has(nodeId)) yCoords[nodeIndexes.get(nodeId)] = yPos; }); } } } /* finally enforce relative placement constraint */ if (constraints.relativePlacementConstraint) { let nodeToDummyForVerticalAlignment = new Map(); let nodeToDummyForHorizontalAlignment = new Map(); let dummyToNodeForVerticalAlignment = new Map(); let dummyToNodeForHorizontalAlignment = new Map(); let dummyPositionsForVerticalAlignment = new Map(); let dummyPositionsForHorizontalAlignment = new Map(); let fixedNodesOnHorizontal = new Set(); let fixedNodesOnVertical = new Set(); // fill maps and sets fixedNodes.forEach(function(nodeId) { fixedNodesOnHorizontal.add(nodeId); fixedNodesOnVertical.add(nodeId); }); if (constraints.alignmentConstraint) { if (constraints.alignmentConstraint.vertical) { let verticalAlignment = constraints.alignmentConstraint.vertical; for (let i = 0; i < verticalAlignment.length; i++) { dummyToNodeForVerticalAlignment.set("dummy" + i, []); verticalAlignment[i].forEach(function(nodeId) { nodeToDummyForVerticalAlignment.set(nodeId, "dummy" + i); dummyToNodeForVerticalAlignment.get("dummy" + i).push(nodeId); if (fixedNodes.has(nodeId)) { fixedNodesOnHorizontal.add("dummy" + i); } }); dummyPositionsForVerticalAlignment.set("dummy" + i, xCoords[nodeIndexes.get(verticalAlignment[i][0])]); } } if (constraints.alignmentConstraint.horizontal) { let horizontalAlignment = constraints.alignmentConstraint.horizontal; for (let i = 0; i < horizontalAlignment.length; i++) { dummyToNodeForHorizontalAlignment.set("dummy" + i, []); horizontalAlignment[i].forEach(function(nodeId) { nodeToDummyForHorizontalAlignment.set(nodeId, "dummy" + i); dummyToNodeForHorizontalAlignment.get("dummy" + i).push(nodeId); if (fixedNodes.has(nodeId)) { fixedNodesOnVertical.add("dummy" + i); } }); dummyPositionsForHorizontalAlignment.set("dummy" + i, yCoords[nodeIndexes.get(horizontalAlignment[i][0])]); } } } // construct horizontal and vertical dags (subgraphs) from overall dag let dagOnHorizontal = new Map(); let dagOnVertical = new Map(); for (let nodeId of dag.keys()) { dag.get(nodeId).forEach(function(adjacent) { let sourceId; let targetNode; if (adjacent["direction"] == "horizontal") { sourceId = nodeToDummyForVerticalAlignment.get(nodeId) ? nodeToDummyForVerticalAlignment.get(nodeId) : nodeId; if (nodeToDummyForVerticalAlignment.get(adjacent.id)) { targetNode = {id: nodeToDummyForVerticalAlignment.get(adjacent.id), gap: adjacent.gap, direction: adjacent.direction}; } else { targetNode = adjacent; } if (dagOnHorizontal.has(sourceId)) { dagOnHorizontal.get(sourceId).push(targetNode); } else { dagOnHorizontal.set(sourceId, [targetNode]); } if (!dagOnHorizontal.has(targetNode.id)) { dagOnHorizontal.set(targetNode.id, []); } } else { sourceId = nodeToDummyForHorizontalAlignment.get(nodeId) ? nodeToDummyForHorizontalAlignment.get(nodeId) : nodeId; if (nodeToDummyForHorizontalAlignment.get(adjacent.id)) { targetNode = {id: nodeToDummyForHorizontalAlignment.get(adjacent.id), gap: adjacent.gap, direction: adjacent.direction}; } else { targetNode = adjacent; } if (dagOnVertical.has(sourceId)) { dagOnVertical.get(sourceId).push(targetNode); } else { dagOnVertical.set(sourceId, [targetNode]); } if (!dagOnVertical.has(targetNode.id)) { dagOnVertical.set(targetNode.id, []); } } }); } // find source nodes of each component in horizontal and vertical dags let undirectedOnHorizontal = dagToUndirected(dagOnHorizontal); let undirectedOnVertical = dagToUndirected(dagOnVertical); let componentsOnHorizontal = findComponents(undirectedOnHorizontal); let componentsOnVertical = findComponents(undirectedOnVertical); let reversedDagOnHorizontal = dagToReversed(dagOnHorizontal); let reversedDagOnVertical = dagToReversed(dagOnVertical); let componentSourcesOnHorizontal = []; let componentSourcesOnVertical = []; componentsOnHorizontal.forEach(function(component, index) { componentSourcesOnHorizontal[index] = []; component.forEach(function(nodeId) { if (reversedDagOnHorizontal.get(nodeId).length == 0) { componentSourcesOnHorizontal[index].push(nodeId); } }); }); componentsOnVertical.forEach(function(component, index) { componentSourcesOnVertical[index] = []; component.forEach(function(nodeId) { if (reversedDagOnVertical.get(nodeId).length == 0) { componentSourcesOnVertical[index].push(nodeId); } }); }); // calculate appropriate positioning for subgraphs let positionMapHorizontal = findAppropriatePositionForRelativePlacement(dagOnHorizontal, "horizontal", fixedNodesOnHorizontal, dummyPositionsForVerticalAlignment, componentSourcesOnHorizontal); let positionMapVertical = findAppropriatePositionForRelativePlacement(dagOnVertical, "vertical", fixedNodesOnVertical, dummyPositionsForHorizontalAlignment, componentSourcesOnVertical); // update positions of the nodes based on relative placement constraints for (let key of positionMapHorizontal.keys()) { if (dummyToNodeForVerticalAlignment.get(key)) { dummyToNodeForVerticalAlignment.get(key).forEach(function(nodeId) { xCoords[nodeIndexes.get(nodeId)] = positionMapHorizontal.get(key); }); } else { xCoords[nodeIndexes.get(key)] = positionMapHorizontal.get(key); } } for (let key of positionMapVertical.keys()) { if (dummyToNodeForHorizontalAlignment.get(key)) { dummyToNodeForHorizontalAlignment.get(key).forEach(function(nodeId) { yCoords[nodeIndexes.get(nodeId)] = positionMapVertical.get(key); }); } else { yCoords[nodeIndexes.get(key)] = positionMapVertical.get(key); } } } } // assign new coordinates to nodes after constraint handling for (let i = 0; i < allNodes.length; i++) { let node = allNodes[i]; if (node.getChild() == null) { node.setCenter(xCoords[nodeIndexes.get(node.id)], yCoords[nodeIndexes.get(node.id)]); } } }; module.exports = ConstraintHandler;