3094 lines
112 KiB
JavaScript
3094 lines
112 KiB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
|
|
if(typeof exports === 'object' && typeof module === 'object')
|
|
module.exports = factory(require("layout-base"));
|
|
else if(typeof define === 'function' && define.amd)
|
|
define(["layout-base"], factory);
|
|
else if(typeof exports === 'object')
|
|
exports["coseBase"] = factory(require("layout-base"));
|
|
else
|
|
root["coseBase"] = factory(root["layoutBase"]);
|
|
})(this, function(__WEBPACK_EXTERNAL_MODULE_0__) {
|
|
return /******/ (function(modules) { // webpackBootstrap
|
|
/******/ // The module cache
|
|
/******/ var installedModules = {};
|
|
/******/
|
|
/******/ // The require function
|
|
/******/ function __webpack_require__(moduleId) {
|
|
/******/
|
|
/******/ // Check if module is in cache
|
|
/******/ if(installedModules[moduleId]) {
|
|
/******/ return installedModules[moduleId].exports;
|
|
/******/ }
|
|
/******/ // Create a new module (and put it into the cache)
|
|
/******/ var module = installedModules[moduleId] = {
|
|
/******/ i: moduleId,
|
|
/******/ l: false,
|
|
/******/ exports: {}
|
|
/******/ };
|
|
/******/
|
|
/******/ // Execute the module function
|
|
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
/******/
|
|
/******/ // Flag the module as loaded
|
|
/******/ module.l = true;
|
|
/******/
|
|
/******/ // Return the exports of the module
|
|
/******/ return module.exports;
|
|
/******/ }
|
|
/******/
|
|
/******/
|
|
/******/ // expose the modules object (__webpack_modules__)
|
|
/******/ __webpack_require__.m = modules;
|
|
/******/
|
|
/******/ // expose the module cache
|
|
/******/ __webpack_require__.c = installedModules;
|
|
/******/
|
|
/******/ // identity function for calling harmony imports with the correct context
|
|
/******/ __webpack_require__.i = function(value) { return value; };
|
|
/******/
|
|
/******/ // define getter function for harmony exports
|
|
/******/ __webpack_require__.d = function(exports, name, getter) {
|
|
/******/ if(!__webpack_require__.o(exports, name)) {
|
|
/******/ Object.defineProperty(exports, name, {
|
|
/******/ configurable: false,
|
|
/******/ enumerable: true,
|
|
/******/ get: getter
|
|
/******/ });
|
|
/******/ }
|
|
/******/ };
|
|
/******/
|
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
/******/ __webpack_require__.n = function(module) {
|
|
/******/ var getter = module && module.__esModule ?
|
|
/******/ function getDefault() { return module['default']; } :
|
|
/******/ function getModuleExports() { return module; };
|
|
/******/ __webpack_require__.d(getter, 'a', getter);
|
|
/******/ return getter;
|
|
/******/ };
|
|
/******/
|
|
/******/ // Object.prototype.hasOwnProperty.call
|
|
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
|
/******/
|
|
/******/ // __webpack_public_path__
|
|
/******/ __webpack_require__.p = "";
|
|
/******/
|
|
/******/ // Load entry module and return exports
|
|
/******/ return __webpack_require__(__webpack_require__.s = 8);
|
|
/******/ })
|
|
/************************************************************************/
|
|
/******/ ([
|
|
/* 0 */
|
|
/***/ (function(module, exports) {
|
|
|
|
module.exports = __WEBPACK_EXTERNAL_MODULE_0__;
|
|
|
|
/***/ }),
|
|
/* 1 */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
|
|
var FDLayoutConstants = __webpack_require__(0).FDLayoutConstants;
|
|
|
|
function CoSEConstants() {}
|
|
|
|
//CoSEConstants inherits static props in FDLayoutConstants
|
|
for (var prop in FDLayoutConstants) {
|
|
CoSEConstants[prop] = FDLayoutConstants[prop];
|
|
}
|
|
|
|
CoSEConstants.DEFAULT_USE_MULTI_LEVEL_SCALING = false;
|
|
CoSEConstants.DEFAULT_RADIAL_SEPARATION = FDLayoutConstants.DEFAULT_EDGE_LENGTH;
|
|
CoSEConstants.DEFAULT_COMPONENT_SEPERATION = 60;
|
|
CoSEConstants.TILE = true;
|
|
CoSEConstants.TILING_PADDING_VERTICAL = 10;
|
|
CoSEConstants.TILING_PADDING_HORIZONTAL = 10;
|
|
CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = true;
|
|
CoSEConstants.ENFORCE_CONSTRAINTS = true;
|
|
CoSEConstants.APPLY_LAYOUT = true;
|
|
CoSEConstants.RELAX_MOVEMENT_ON_CONSTRAINTS = true;
|
|
CoSEConstants.TREE_REDUCTION_ON_INCREMENTAL = true; // this should be set to false if there will be a constraint
|
|
// This constant is for differentiating whether actual layout algorithm that uses cose-base wants to apply only incremental layout or
|
|
// an incremental layout on top of a randomized layout. If it is only incremental layout, then this constant should be true.
|
|
CoSEConstants.PURE_INCREMENTAL = CoSEConstants.DEFAULT_INCREMENTAL;
|
|
|
|
module.exports = CoSEConstants;
|
|
|
|
/***/ }),
|
|
/* 2 */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
|
|
var FDLayoutEdge = __webpack_require__(0).FDLayoutEdge;
|
|
|
|
function CoSEEdge(source, target, vEdge) {
|
|
FDLayoutEdge.call(this, source, target, vEdge);
|
|
}
|
|
|
|
CoSEEdge.prototype = Object.create(FDLayoutEdge.prototype);
|
|
for (var prop in FDLayoutEdge) {
|
|
CoSEEdge[prop] = FDLayoutEdge[prop];
|
|
}
|
|
|
|
module.exports = CoSEEdge;
|
|
|
|
/***/ }),
|
|
/* 3 */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
|
|
var LGraph = __webpack_require__(0).LGraph;
|
|
|
|
function CoSEGraph(parent, graphMgr, vGraph) {
|
|
LGraph.call(this, parent, graphMgr, vGraph);
|
|
}
|
|
|
|
CoSEGraph.prototype = Object.create(LGraph.prototype);
|
|
for (var prop in LGraph) {
|
|
CoSEGraph[prop] = LGraph[prop];
|
|
}
|
|
|
|
module.exports = CoSEGraph;
|
|
|
|
/***/ }),
|
|
/* 4 */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
|
|
var LGraphManager = __webpack_require__(0).LGraphManager;
|
|
|
|
function CoSEGraphManager(layout) {
|
|
LGraphManager.call(this, layout);
|
|
}
|
|
|
|
CoSEGraphManager.prototype = Object.create(LGraphManager.prototype);
|
|
for (var prop in LGraphManager) {
|
|
CoSEGraphManager[prop] = LGraphManager[prop];
|
|
}
|
|
|
|
module.exports = CoSEGraphManager;
|
|
|
|
/***/ }),
|
|
/* 5 */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
|
|
var FDLayoutNode = __webpack_require__(0).FDLayoutNode;
|
|
var IMath = __webpack_require__(0).IMath;
|
|
|
|
function CoSENode(gm, loc, size, vNode) {
|
|
FDLayoutNode.call(this, gm, loc, size, vNode);
|
|
}
|
|
|
|
CoSENode.prototype = Object.create(FDLayoutNode.prototype);
|
|
for (var prop in FDLayoutNode) {
|
|
CoSENode[prop] = FDLayoutNode[prop];
|
|
}
|
|
|
|
CoSENode.prototype.calculateDisplacement = function () {
|
|
var layout = this.graphManager.getLayout();
|
|
// this check is for compound nodes that contain fixed nodes
|
|
if (this.getChild() != null && this.fixedNodeWeight) {
|
|
this.displacementX += layout.coolingFactor * (this.springForceX + this.repulsionForceX + this.gravitationForceX) / this.fixedNodeWeight;
|
|
this.displacementY += layout.coolingFactor * (this.springForceY + this.repulsionForceY + this.gravitationForceY) / this.fixedNodeWeight;
|
|
} else {
|
|
this.displacementX += layout.coolingFactor * (this.springForceX + this.repulsionForceX + this.gravitationForceX) / this.noOfChildren;
|
|
this.displacementY += layout.coolingFactor * (this.springForceY + this.repulsionForceY + this.gravitationForceY) / this.noOfChildren;
|
|
}
|
|
|
|
if (Math.abs(this.displacementX) > layout.coolingFactor * layout.maxNodeDisplacement) {
|
|
this.displacementX = layout.coolingFactor * layout.maxNodeDisplacement * IMath.sign(this.displacementX);
|
|
}
|
|
|
|
if (Math.abs(this.displacementY) > layout.coolingFactor * layout.maxNodeDisplacement) {
|
|
this.displacementY = layout.coolingFactor * layout.maxNodeDisplacement * IMath.sign(this.displacementY);
|
|
}
|
|
|
|
// non-empty compound node, propogate movement to children as well
|
|
if (this.child && this.child.getNodes().length > 0) {
|
|
this.propogateDisplacementToChildren(this.displacementX, this.displacementY);
|
|
}
|
|
};
|
|
|
|
CoSENode.prototype.propogateDisplacementToChildren = function (dX, dY) {
|
|
var nodes = this.getChild().getNodes();
|
|
var node;
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
node = nodes[i];
|
|
if (node.getChild() == null) {
|
|
node.displacementX += dX;
|
|
node.displacementY += dY;
|
|
} else {
|
|
node.propogateDisplacementToChildren(dX, dY);
|
|
}
|
|
}
|
|
};
|
|
|
|
CoSENode.prototype.move = function () {
|
|
var layout = this.graphManager.getLayout();
|
|
|
|
// a simple node or an empty compound node, move it
|
|
if (this.child == null || this.child.getNodes().length == 0) {
|
|
this.moveBy(this.displacementX, this.displacementY);
|
|
|
|
layout.totalDisplacement += Math.abs(this.displacementX) + Math.abs(this.displacementY);
|
|
}
|
|
|
|
this.springForceX = 0;
|
|
this.springForceY = 0;
|
|
this.repulsionForceX = 0;
|
|
this.repulsionForceY = 0;
|
|
this.gravitationForceX = 0;
|
|
this.gravitationForceY = 0;
|
|
this.displacementX = 0;
|
|
this.displacementY = 0;
|
|
};
|
|
|
|
CoSENode.prototype.setPred1 = function (pred1) {
|
|
this.pred1 = pred1;
|
|
};
|
|
|
|
CoSENode.prototype.getPred1 = function () {
|
|
return pred1;
|
|
};
|
|
|
|
CoSENode.prototype.getPred2 = function () {
|
|
return pred2;
|
|
};
|
|
|
|
CoSENode.prototype.setNext = function (next) {
|
|
this.next = next;
|
|
};
|
|
|
|
CoSENode.prototype.getNext = function () {
|
|
return next;
|
|
};
|
|
|
|
CoSENode.prototype.setProcessed = function (processed) {
|
|
this.processed = processed;
|
|
};
|
|
|
|
CoSENode.prototype.isProcessed = function () {
|
|
return processed;
|
|
};
|
|
|
|
module.exports = CoSENode;
|
|
|
|
/***/ }),
|
|
/* 6 */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
|
|
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
|
|
|
|
var CoSEConstants = __webpack_require__(1);
|
|
var LinkedList = __webpack_require__(0).LinkedList;
|
|
var Matrix = __webpack_require__(0).Matrix;
|
|
var SVD = __webpack_require__(0).SVD;
|
|
|
|
function ConstraintHandler() {}
|
|
|
|
ConstraintHandler.handleConstraints = function (layout) {
|
|
// let layout = this.graphManager.getLayout();
|
|
|
|
// get constraints from layout
|
|
var constraints = {};
|
|
constraints.fixedNodeConstraint = layout.constraints.fixedNodeConstraint;
|
|
constraints.alignmentConstraint = layout.constraints.alignmentConstraint;
|
|
constraints.relativePlacementConstraint = layout.constraints.relativePlacementConstraint;
|
|
|
|
var idToNodeMap = new Map();
|
|
var nodeIndexes = new Map();
|
|
var xCoords = [];
|
|
var yCoords = [];
|
|
|
|
var allNodes = layout.getAllNodes();
|
|
var index = 0;
|
|
// fill index map and coordinates
|
|
for (var i = 0; i < allNodes.length; i++) {
|
|
var 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
|
|
var calculatePositionDiff = function calculatePositionDiff(pos1, pos2) {
|
|
return { x: pos1.x - pos2.x, y: pos1.y - pos2.y };
|
|
};
|
|
|
|
// calculate average position of the nodes
|
|
var calculateAvgPosition = function calculateAvgPosition(nodeIdSet) {
|
|
var xPosSum = 0;
|
|
var 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
|
|
var findAppropriatePositionForRelativePlacement = function findAppropriatePositionForRelativePlacement(graph, direction, fixedNodes, dummyPositions, componentSources) {
|
|
|
|
// find union of two sets
|
|
function setUnion(setA, setB) {
|
|
var union = new Set(setA);
|
|
var _iteratorNormalCompletion = true;
|
|
var _didIteratorError = false;
|
|
var _iteratorError = undefined;
|
|
|
|
try {
|
|
for (var _iterator = setB[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
|
|
var elem = _step.value;
|
|
|
|
union.add(elem);
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError = true;
|
|
_iteratorError = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion && _iterator.return) {
|
|
_iterator.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError) {
|
|
throw _iteratorError;
|
|
}
|
|
}
|
|
}
|
|
|
|
return union;
|
|
}
|
|
|
|
// find indegree count for each node
|
|
var 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);
|
|
});
|
|
});
|
|
|
|
var positionMap = new Map(); // keeps the position for each node
|
|
var pastMap = new Map(); // keeps the predecessors(past) of a node
|
|
var 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) {
|
|
var fixedIds = [];
|
|
component.forEach(function (nodeId) {
|
|
if (fixedNodes.has(nodeId)) {
|
|
fixedIds.push(nodeId);
|
|
}
|
|
});
|
|
if (fixedIds.length > 0) {
|
|
var 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 {
|
|
var _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
|
|
|
|
var _loop = function _loop() {
|
|
var currentNode = queue.shift();
|
|
var neighbors = graph.get(currentNode);
|
|
neighbors.forEach(function (neighbor) {
|
|
if (positionMap.get(neighbor.id) < positionMap.get(currentNode) + neighbor.gap) {
|
|
if (fixedNodes && fixedNodes.has(neighbor.id)) {
|
|
var fixedPosition = void 0;
|
|
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) {
|
|
var 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)));
|
|
}
|
|
});
|
|
};
|
|
|
|
while (queue.length != 0) {
|
|
_loop();
|
|
}
|
|
|
|
// readjust position of the nodes after enforcement
|
|
if (fixedNodes) {
|
|
// find indegree count for each node
|
|
var sinkNodes = new Set();
|
|
|
|
graph.forEach(function (value, key) {
|
|
if (value.length == 0) {
|
|
sinkNodes.add(key);
|
|
}
|
|
});
|
|
|
|
var _components = [];
|
|
pastMap.forEach(function (value, key) {
|
|
if (sinkNodes.has(key)) {
|
|
var isFixedComponent = false;
|
|
var _iteratorNormalCompletion2 = true;
|
|
var _didIteratorError2 = false;
|
|
var _iteratorError2 = undefined;
|
|
|
|
try {
|
|
for (var _iterator2 = value[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
|
|
var nodeId = _step2.value;
|
|
|
|
if (fixedNodes.has(nodeId)) {
|
|
isFixedComponent = true;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError2 = true;
|
|
_iteratorError2 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion2 && _iterator2.return) {
|
|
_iterator2.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError2) {
|
|
throw _iteratorError2;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isFixedComponent) {
|
|
var isExist = false;
|
|
var existAt = void 0;
|
|
_components.forEach(function (component, index) {
|
|
if (component.has([].concat(_toConsumableArray(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) {
|
|
var minBefore = Number.POSITIVE_INFINITY;
|
|
var minAfter = Number.POSITIVE_INFINITY;
|
|
var maxBefore = Number.NEGATIVE_INFINITY;
|
|
var maxAfter = Number.NEGATIVE_INFINITY;
|
|
|
|
var _iteratorNormalCompletion3 = true;
|
|
var _didIteratorError3 = false;
|
|
var _iteratorError3 = undefined;
|
|
|
|
try {
|
|
for (var _iterator3 = component[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
|
|
var nodeId = _step3.value;
|
|
|
|
var posBefore = void 0;
|
|
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);
|
|
}
|
|
var posAfter = positionMap.get(nodeId);
|
|
if (posBefore < minBefore) {
|
|
minBefore = posBefore;
|
|
}
|
|
if (posBefore > maxBefore) {
|
|
maxBefore = posBefore;
|
|
}
|
|
if (posAfter < minAfter) {
|
|
minAfter = posAfter;
|
|
}
|
|
if (posAfter > maxAfter) {
|
|
maxAfter = posAfter;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError3 = true;
|
|
_iteratorError3 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion3 && _iterator3.return) {
|
|
_iterator3.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError3) {
|
|
throw _iteratorError3;
|
|
}
|
|
}
|
|
}
|
|
|
|
var diff = (minBefore + maxBefore) / 2 - (minAfter + maxAfter) / 2;
|
|
|
|
var _iteratorNormalCompletion4 = true;
|
|
var _didIteratorError4 = false;
|
|
var _iteratorError4 = undefined;
|
|
|
|
try {
|
|
for (var _iterator4 = component[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
|
|
var _nodeId = _step4.value;
|
|
|
|
positionMap.set(_nodeId, positionMap.get(_nodeId) + diff);
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError4 = true;
|
|
_iteratorError4 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion4 && _iterator4.return) {
|
|
_iterator4.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError4) {
|
|
throw _iteratorError4;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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
|
|
var applyReflectionForRelativePlacement = function applyReflectionForRelativePlacement(relativePlacementConstraints) {
|
|
// variables to count votes
|
|
var reflectOnY = 0,
|
|
notReflectOnY = 0;
|
|
var 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 (var _i = 0; _i < nodeIndexes.size; _i++) {
|
|
xCoords[_i] = -1 * xCoords[_i];
|
|
yCoords[_i] = -1 * yCoords[_i];
|
|
}
|
|
} else if (reflectOnY > notReflectOnY) {
|
|
for (var _i2 = 0; _i2 < nodeIndexes.size; _i2++) {
|
|
xCoords[_i2] = -1 * xCoords[_i2];
|
|
}
|
|
} else if (reflectOnX > notReflectOnX) {
|
|
for (var _i3 = 0; _i3 < nodeIndexes.size; _i3++) {
|
|
yCoords[_i3] = -1 * yCoords[_i3];
|
|
}
|
|
}
|
|
};
|
|
|
|
// find weakly connected components in undirected graph
|
|
var findComponents = function findComponents(graph) {
|
|
// find weakly connected components in dag
|
|
var components = [];
|
|
var queue = new LinkedList();
|
|
var visited = new Set();
|
|
var count = 0;
|
|
|
|
graph.forEach(function (value, key) {
|
|
if (!visited.has(key)) {
|
|
components[count] = [];
|
|
var _currentNode = key;
|
|
queue.push(_currentNode);
|
|
visited.add(_currentNode);
|
|
components[count].push(_currentNode);
|
|
|
|
while (queue.length != 0) {
|
|
_currentNode = queue.shift();
|
|
var 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
|
|
var dagToUndirected = function dagToUndirected(dag) {
|
|
var 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
|
|
var dagToReversed = function dagToReversed(dag) {
|
|
var 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 */
|
|
|
|
var targetMatrix = []; // A - target configuration
|
|
var sourceMatrix = []; // B - source configuration
|
|
var standardTransformation = false; // false for no transformation, true for standart (Procrustes) transformation (rotation and/or reflection)
|
|
var reflectionType = false; // false/true for reflection check, 'reflectOnX', 'reflectOnY' or 'reflectOnBoth' for reflection type if necessary
|
|
var fixedNodes = new Set();
|
|
var dag = new Map(); // adjacency list to keep directed acyclic graph (dag) that consists of relative placement constraints
|
|
var dagUndirected = new Map(); // undirected version of the dag
|
|
var 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) {
|
|
(function () {
|
|
// then check alignment constraint
|
|
var count = 0;
|
|
if (constraints.alignmentConstraint.vertical) {
|
|
var verticalAlign = constraints.alignmentConstraint.vertical;
|
|
|
|
var _loop2 = function _loop2(_i4) {
|
|
var alignmentSet = new Set();
|
|
verticalAlign[_i4].forEach(function (nodeId) {
|
|
alignmentSet.add(nodeId);
|
|
});
|
|
var intersection = new Set([].concat(_toConsumableArray(alignmentSet)).filter(function (x) {
|
|
return fixedNodes.has(x);
|
|
}));
|
|
var xPos = void 0;
|
|
if (intersection.size > 0) xPos = xCoords[nodeIndexes.get(intersection.values().next().value)];else xPos = calculateAvgPosition(alignmentSet).x;
|
|
|
|
verticalAlign[_i4].forEach(function (nodeId) {
|
|
targetMatrix[count] = [xPos, yCoords[nodeIndexes.get(nodeId)]];
|
|
sourceMatrix[count] = [xCoords[nodeIndexes.get(nodeId)], yCoords[nodeIndexes.get(nodeId)]];
|
|
count++;
|
|
});
|
|
};
|
|
|
|
for (var _i4 = 0; _i4 < verticalAlign.length; _i4++) {
|
|
_loop2(_i4);
|
|
}
|
|
standardTransformation = true;
|
|
}
|
|
if (constraints.alignmentConstraint.horizontal) {
|
|
var horizontalAlign = constraints.alignmentConstraint.horizontal;
|
|
|
|
var _loop3 = function _loop3(_i5) {
|
|
var alignmentSet = new Set();
|
|
horizontalAlign[_i5].forEach(function (nodeId) {
|
|
alignmentSet.add(nodeId);
|
|
});
|
|
var intersection = new Set([].concat(_toConsumableArray(alignmentSet)).filter(function (x) {
|
|
return fixedNodes.has(x);
|
|
}));
|
|
var yPos = void 0;
|
|
if (intersection.size > 0) yPos = xCoords[nodeIndexes.get(intersection.values().next().value)];else yPos = calculateAvgPosition(alignmentSet).y;
|
|
|
|
horizontalAlign[_i5].forEach(function (nodeId) {
|
|
targetMatrix[count] = [xCoords[nodeIndexes.get(nodeId)], yPos];
|
|
sourceMatrix[count] = [xCoords[nodeIndexes.get(nodeId)], yCoords[nodeIndexes.get(nodeId)]];
|
|
count++;
|
|
});
|
|
};
|
|
|
|
for (var _i5 = 0; _i5 < horizontalAlign.length; _i5++) {
|
|
_loop3(_i5);
|
|
}
|
|
standardTransformation = true;
|
|
}
|
|
if (constraints.relativePlacementConstraint) {
|
|
reflectionType = true;
|
|
}
|
|
})();
|
|
} else if (constraints.relativePlacementConstraint) {
|
|
// finally check relative placement constraint
|
|
// find largest component in dag
|
|
var largestComponentSize = 0;
|
|
var largestComponentIndex = 0;
|
|
for (var _i6 = 0; _i6 < components.length; _i6++) {
|
|
if (components[_i6].length > largestComponentSize) {
|
|
largestComponentSize = components[_i6].length;
|
|
largestComponentIndex = _i6;
|
|
}
|
|
}
|
|
// 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
|
|
var subGraphOnHorizontal = new Map();
|
|
var subGraphOnVertical = new Map();
|
|
var 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
|
|
var positionMapHorizontal = findAppropriatePositionForRelativePlacement(subGraphOnHorizontal, "horizontal");
|
|
var 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 */
|
|
var transformationMatrix = void 0;
|
|
var targetMatrixTranspose = Matrix.transpose(targetMatrix); // A'
|
|
var sourceMatrixTranspose = Matrix.transpose(sourceMatrix); // B'
|
|
|
|
// centralize transpose matrices
|
|
for (var _i7 = 0; _i7 < targetMatrixTranspose.length; _i7++) {
|
|
targetMatrixTranspose[_i7] = Matrix.multGamma(targetMatrixTranspose[_i7]);
|
|
sourceMatrixTranspose[_i7] = Matrix.multGamma(sourceMatrixTranspose[_i7]);
|
|
}
|
|
|
|
// do actual calculation for transformation matrix
|
|
var tempMatrix = Matrix.multMat(targetMatrixTranspose, Matrix.transpose(sourceMatrixTranspose)); // tempMatrix = A'B
|
|
var 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 (var _i8 = 0; _i8 < nodeIndexes.size; _i8++) {
|
|
var temp1 = [xCoords[_i8], yCoords[_i8]];
|
|
var temp2 = [transformationMatrix[0][0], transformationMatrix[1][0]];
|
|
var temp3 = [transformationMatrix[0][1], transformationMatrix[1][1]];
|
|
xCoords[_i8] = Matrix.dotProduct(temp1, temp2);
|
|
yCoords[_i8] = 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) {
|
|
var translationAmount = { x: 0, y: 0 };
|
|
constraints.fixedNodeConstraint.forEach(function (nodeData, i) {
|
|
var posInTheory = { x: xCoords[nodeIndexes.get(nodeData.nodeId)], y: yCoords[nodeIndexes.get(nodeData.nodeId)] };
|
|
var posDesired = nodeData.position;
|
|
var 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) {
|
|
var xAlign = constraints.alignmentConstraint.vertical;
|
|
|
|
var _loop4 = function _loop4(_i9) {
|
|
var alignmentSet = new Set();
|
|
xAlign[_i9].forEach(function (nodeId) {
|
|
alignmentSet.add(nodeId);
|
|
});
|
|
var intersection = new Set([].concat(_toConsumableArray(alignmentSet)).filter(function (x) {
|
|
return fixedNodes.has(x);
|
|
}));
|
|
var xPos = void 0;
|
|
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;
|
|
});
|
|
};
|
|
|
|
for (var _i9 = 0; _i9 < xAlign.length; _i9++) {
|
|
_loop4(_i9);
|
|
}
|
|
}
|
|
if (constraints.alignmentConstraint.horizontal) {
|
|
var yAlign = constraints.alignmentConstraint.horizontal;
|
|
|
|
var _loop5 = function _loop5(_i10) {
|
|
var alignmentSet = new Set();
|
|
yAlign[_i10].forEach(function (nodeId) {
|
|
alignmentSet.add(nodeId);
|
|
});
|
|
var intersection = new Set([].concat(_toConsumableArray(alignmentSet)).filter(function (x) {
|
|
return fixedNodes.has(x);
|
|
}));
|
|
var yPos = void 0;
|
|
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;
|
|
});
|
|
};
|
|
|
|
for (var _i10 = 0; _i10 < yAlign.length; _i10++) {
|
|
_loop5(_i10);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* finally enforce relative placement constraint */
|
|
|
|
if (constraints.relativePlacementConstraint) {
|
|
(function () {
|
|
var nodeToDummyForVerticalAlignment = new Map();
|
|
var nodeToDummyForHorizontalAlignment = new Map();
|
|
var dummyToNodeForVerticalAlignment = new Map();
|
|
var dummyToNodeForHorizontalAlignment = new Map();
|
|
var dummyPositionsForVerticalAlignment = new Map();
|
|
var dummyPositionsForHorizontalAlignment = new Map();
|
|
var fixedNodesOnHorizontal = new Set();
|
|
var fixedNodesOnVertical = new Set();
|
|
|
|
// fill maps and sets
|
|
fixedNodes.forEach(function (nodeId) {
|
|
fixedNodesOnHorizontal.add(nodeId);
|
|
fixedNodesOnVertical.add(nodeId);
|
|
});
|
|
|
|
if (constraints.alignmentConstraint) {
|
|
if (constraints.alignmentConstraint.vertical) {
|
|
var verticalAlignment = constraints.alignmentConstraint.vertical;
|
|
|
|
var _loop6 = function _loop6(_i11) {
|
|
dummyToNodeForVerticalAlignment.set("dummy" + _i11, []);
|
|
verticalAlignment[_i11].forEach(function (nodeId) {
|
|
nodeToDummyForVerticalAlignment.set(nodeId, "dummy" + _i11);
|
|
dummyToNodeForVerticalAlignment.get("dummy" + _i11).push(nodeId);
|
|
if (fixedNodes.has(nodeId)) {
|
|
fixedNodesOnHorizontal.add("dummy" + _i11);
|
|
}
|
|
});
|
|
dummyPositionsForVerticalAlignment.set("dummy" + _i11, xCoords[nodeIndexes.get(verticalAlignment[_i11][0])]);
|
|
};
|
|
|
|
for (var _i11 = 0; _i11 < verticalAlignment.length; _i11++) {
|
|
_loop6(_i11);
|
|
}
|
|
}
|
|
if (constraints.alignmentConstraint.horizontal) {
|
|
var horizontalAlignment = constraints.alignmentConstraint.horizontal;
|
|
|
|
var _loop7 = function _loop7(_i12) {
|
|
dummyToNodeForHorizontalAlignment.set("dummy" + _i12, []);
|
|
horizontalAlignment[_i12].forEach(function (nodeId) {
|
|
nodeToDummyForHorizontalAlignment.set(nodeId, "dummy" + _i12);
|
|
dummyToNodeForHorizontalAlignment.get("dummy" + _i12).push(nodeId);
|
|
if (fixedNodes.has(nodeId)) {
|
|
fixedNodesOnVertical.add("dummy" + _i12);
|
|
}
|
|
});
|
|
dummyPositionsForHorizontalAlignment.set("dummy" + _i12, yCoords[nodeIndexes.get(horizontalAlignment[_i12][0])]);
|
|
};
|
|
|
|
for (var _i12 = 0; _i12 < horizontalAlignment.length; _i12++) {
|
|
_loop7(_i12);
|
|
}
|
|
}
|
|
}
|
|
|
|
// construct horizontal and vertical dags (subgraphs) from overall dag
|
|
var dagOnHorizontal = new Map();
|
|
var dagOnVertical = new Map();
|
|
|
|
var _loop8 = function _loop8(nodeId) {
|
|
dag.get(nodeId).forEach(function (adjacent) {
|
|
var sourceId = void 0;
|
|
var targetNode = void 0;
|
|
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, []);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
var _iteratorNormalCompletion5 = true;
|
|
var _didIteratorError5 = false;
|
|
var _iteratorError5 = undefined;
|
|
|
|
try {
|
|
for (var _iterator5 = dag.keys()[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
|
|
var nodeId = _step5.value;
|
|
|
|
_loop8(nodeId);
|
|
}
|
|
|
|
// find source nodes of each component in horizontal and vertical dags
|
|
} catch (err) {
|
|
_didIteratorError5 = true;
|
|
_iteratorError5 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion5 && _iterator5.return) {
|
|
_iterator5.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError5) {
|
|
throw _iteratorError5;
|
|
}
|
|
}
|
|
}
|
|
|
|
var undirectedOnHorizontal = dagToUndirected(dagOnHorizontal);
|
|
var undirectedOnVertical = dagToUndirected(dagOnVertical);
|
|
var componentsOnHorizontal = findComponents(undirectedOnHorizontal);
|
|
var componentsOnVertical = findComponents(undirectedOnVertical);
|
|
var reversedDagOnHorizontal = dagToReversed(dagOnHorizontal);
|
|
var reversedDagOnVertical = dagToReversed(dagOnVertical);
|
|
var componentSourcesOnHorizontal = [];
|
|
var 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
|
|
var positionMapHorizontal = findAppropriatePositionForRelativePlacement(dagOnHorizontal, "horizontal", fixedNodesOnHorizontal, dummyPositionsForVerticalAlignment, componentSourcesOnHorizontal);
|
|
var positionMapVertical = findAppropriatePositionForRelativePlacement(dagOnVertical, "vertical", fixedNodesOnVertical, dummyPositionsForHorizontalAlignment, componentSourcesOnVertical);
|
|
|
|
// update positions of the nodes based on relative placement constraints
|
|
|
|
var _loop9 = function _loop9(key) {
|
|
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);
|
|
}
|
|
};
|
|
|
|
var _iteratorNormalCompletion6 = true;
|
|
var _didIteratorError6 = false;
|
|
var _iteratorError6 = undefined;
|
|
|
|
try {
|
|
for (var _iterator6 = positionMapHorizontal.keys()[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
|
|
var key = _step6.value;
|
|
|
|
_loop9(key);
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError6 = true;
|
|
_iteratorError6 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion6 && _iterator6.return) {
|
|
_iterator6.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError6) {
|
|
throw _iteratorError6;
|
|
}
|
|
}
|
|
}
|
|
|
|
var _loop10 = function _loop10(key) {
|
|
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);
|
|
}
|
|
};
|
|
|
|
var _iteratorNormalCompletion7 = true;
|
|
var _didIteratorError7 = false;
|
|
var _iteratorError7 = undefined;
|
|
|
|
try {
|
|
for (var _iterator7 = positionMapVertical.keys()[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
|
|
var key = _step7.value;
|
|
|
|
_loop10(key);
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError7 = true;
|
|
_iteratorError7 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion7 && _iterator7.return) {
|
|
_iterator7.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError7) {
|
|
throw _iteratorError7;
|
|
}
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
}
|
|
|
|
// assign new coordinates to nodes after constraint handling
|
|
for (var _i13 = 0; _i13 < allNodes.length; _i13++) {
|
|
var _node = allNodes[_i13];
|
|
if (_node.getChild() == null) {
|
|
_node.setCenter(xCoords[nodeIndexes.get(_node.id)], yCoords[nodeIndexes.get(_node.id)]);
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = ConstraintHandler;
|
|
|
|
/***/ }),
|
|
/* 7 */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
|
|
var FDLayout = __webpack_require__(0).FDLayout;
|
|
var CoSEGraphManager = __webpack_require__(4);
|
|
var CoSEGraph = __webpack_require__(3);
|
|
var CoSENode = __webpack_require__(5);
|
|
var CoSEEdge = __webpack_require__(2);
|
|
var CoSEConstants = __webpack_require__(1);
|
|
var ConstraintHandler = __webpack_require__(6);
|
|
var FDLayoutConstants = __webpack_require__(0).FDLayoutConstants;
|
|
var LayoutConstants = __webpack_require__(0).LayoutConstants;
|
|
var Point = __webpack_require__(0).Point;
|
|
var PointD = __webpack_require__(0).PointD;
|
|
var DimensionD = __webpack_require__(0).DimensionD;
|
|
var Layout = __webpack_require__(0).Layout;
|
|
var Integer = __webpack_require__(0).Integer;
|
|
var IGeometry = __webpack_require__(0).IGeometry;
|
|
var LGraph = __webpack_require__(0).LGraph;
|
|
var Transform = __webpack_require__(0).Transform;
|
|
var LinkedList = __webpack_require__(0).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(function (x) {
|
|
return 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(function (x) {
|
|
return 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(function (x) {
|
|
return 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 calculateCompoundWeight(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) {
|
|
var 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) {
|
|
var nodeIdLeft = nodeToDummyForVerticalAlignment.has(constraint.left) ? nodeToDummyForVerticalAlignment.get(constraint.left) : constraint.left;
|
|
var 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 {
|
|
var nodeIdTop = nodeToDummyForHorizontalAlignment.has(constraint.top) ? nodeToDummyForHorizontalAlignment.get(constraint.top) : constraint.top;
|
|
var 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 constructComponents(graph, fixedNodes) {
|
|
var components = [];
|
|
var isFixed = [];
|
|
var queue = new LinkedList();
|
|
var visited = new Set();
|
|
var count = 0;
|
|
|
|
graph.forEach(function (value, key) {
|
|
if (!visited.has(key)) {
|
|
components[count] = [];
|
|
isFixed[count] = false;
|
|
var 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;
|
|
}
|
|
var 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.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.tileNodes = function (nodes, minWidth) {
|
|
var verticalPadding = CoSEConstants.TILING_PADDING_VERTICAL;
|
|
var horizontalPadding = CoSEConstants.TILING_PADDING_HORIZONTAL;
|
|
var organization = {
|
|
rows: [],
|
|
rowWidth: [],
|
|
rowHeight: [],
|
|
width: 0,
|
|
height: minWidth, // assume minHeight equals to minWidth
|
|
verticalPadding: verticalPadding,
|
|
horizontalPadding: horizontalPadding,
|
|
centerX: 0,
|
|
centerY: 0
|
|
};
|
|
|
|
// Sort the nodes in ascending order of their areas
|
|
nodes.sort(function (n1, n2) {
|
|
if (n1.rect.width * n1.rect.height > n2.rect.width * n2.rect.height) return -1;
|
|
if (n1.rect.width * n1.rect.height < n2.rect.width * n2.rect.height) return 1;
|
|
return 0;
|
|
});
|
|
|
|
// Create the organization -> calculate compound center
|
|
var sumCenterX = 0;
|
|
var sumCenterY = 0;
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
var lNode = nodes[i];
|
|
|
|
sumCenterX += lNode.getCenterX();
|
|
sumCenterY += lNode.getCenterY();
|
|
}
|
|
|
|
organization.centerX = sumCenterX / nodes.length;
|
|
organization.centerY = sumCenterY / nodes.length;
|
|
|
|
// Create the organization -> tile members
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
var lNode = nodes[i];
|
|
|
|
if (organization.rows.length == 0) {
|
|
this.insertNodeToRow(organization, lNode, 0, minWidth);
|
|
} else if (this.canAddHorizontal(organization, lNode.rect.width, lNode.rect.height)) {
|
|
this.insertNodeToRow(organization, lNode, this.getShortestRowIndex(organization), minWidth);
|
|
} else {
|
|
this.insertNodeToRow(organization, lNode, organization.rows.length, minWidth);
|
|
}
|
|
|
|
this.shiftToLastRow(organization);
|
|
}
|
|
|
|
return organization;
|
|
};
|
|
|
|
CoSELayout.prototype.insertNodeToRow = function (organization, node, rowIndex, minWidth) {
|
|
var minCompoundSize = minWidth;
|
|
|
|
// Add new row if needed
|
|
if (rowIndex == organization.rows.length) {
|
|
var secondDimension = [];
|
|
|
|
organization.rows.push(secondDimension);
|
|
organization.rowWidth.push(minCompoundSize);
|
|
organization.rowHeight.push(0);
|
|
}
|
|
|
|
// Update row width
|
|
var w = organization.rowWidth[rowIndex] + node.rect.width;
|
|
|
|
if (organization.rows[rowIndex].length > 0) {
|
|
w += organization.horizontalPadding;
|
|
}
|
|
|
|
organization.rowWidth[rowIndex] = w;
|
|
// Update compound width
|
|
if (organization.width < w) {
|
|
organization.width = w;
|
|
}
|
|
|
|
// Update height
|
|
var h = node.rect.height;
|
|
if (rowIndex > 0) h += organization.verticalPadding;
|
|
|
|
var extraHeight = 0;
|
|
if (h > organization.rowHeight[rowIndex]) {
|
|
extraHeight = organization.rowHeight[rowIndex];
|
|
organization.rowHeight[rowIndex] = h;
|
|
extraHeight = organization.rowHeight[rowIndex] - extraHeight;
|
|
}
|
|
|
|
organization.height += extraHeight;
|
|
|
|
// Insert node
|
|
organization.rows[rowIndex].push(node);
|
|
};
|
|
|
|
//Scans the rows of an organization and returns the one with the min width
|
|
CoSELayout.prototype.getShortestRowIndex = function (organization) {
|
|
var r = -1;
|
|
var min = Number.MAX_VALUE;
|
|
|
|
for (var i = 0; i < organization.rows.length; i++) {
|
|
if (organization.rowWidth[i] < min) {
|
|
r = i;
|
|
min = organization.rowWidth[i];
|
|
}
|
|
}
|
|
return r;
|
|
};
|
|
|
|
//Scans the rows of an organization and returns the one with the max width
|
|
CoSELayout.prototype.getLongestRowIndex = function (organization) {
|
|
var r = -1;
|
|
var max = Number.MIN_VALUE;
|
|
|
|
for (var i = 0; i < organization.rows.length; i++) {
|
|
|
|
if (organization.rowWidth[i] > max) {
|
|
r = i;
|
|
max = organization.rowWidth[i];
|
|
}
|
|
}
|
|
|
|
return r;
|
|
};
|
|
|
|
/**
|
|
* This method checks whether adding extra width to the organization violates
|
|
* the aspect ratio(1) or not.
|
|
*/
|
|
CoSELayout.prototype.canAddHorizontal = function (organization, extraWidth, extraHeight) {
|
|
|
|
var sri = this.getShortestRowIndex(organization);
|
|
|
|
if (sri < 0) {
|
|
return true;
|
|
}
|
|
|
|
var min = organization.rowWidth[sri];
|
|
|
|
if (min + organization.horizontalPadding + extraWidth <= organization.width) return true;
|
|
|
|
var hDiff = 0;
|
|
|
|
// Adding to an existing row
|
|
if (organization.rowHeight[sri] < extraHeight) {
|
|
if (sri > 0) hDiff = extraHeight + organization.verticalPadding - organization.rowHeight[sri];
|
|
}
|
|
|
|
var add_to_row_ratio;
|
|
if (organization.width - min >= extraWidth + organization.horizontalPadding) {
|
|
add_to_row_ratio = (organization.height + hDiff) / (min + extraWidth + organization.horizontalPadding);
|
|
} else {
|
|
add_to_row_ratio = (organization.height + hDiff) / organization.width;
|
|
}
|
|
|
|
// Adding a new row for this node
|
|
hDiff = extraHeight + organization.verticalPadding;
|
|
var add_new_row_ratio;
|
|
if (organization.width < extraWidth) {
|
|
add_new_row_ratio = (organization.height + hDiff) / extraWidth;
|
|
} else {
|
|
add_new_row_ratio = (organization.height + hDiff) / organization.width;
|
|
}
|
|
|
|
if (add_new_row_ratio < 1) add_new_row_ratio = 1 / add_new_row_ratio;
|
|
|
|
if (add_to_row_ratio < 1) add_to_row_ratio = 1 / add_to_row_ratio;
|
|
|
|
return add_to_row_ratio < add_new_row_ratio;
|
|
};
|
|
|
|
//If moving the last node from the longest row and adding it to the last
|
|
//row makes the bounding box smaller, do it.
|
|
CoSELayout.prototype.shiftToLastRow = function (organization) {
|
|
var longest = this.getLongestRowIndex(organization);
|
|
var last = organization.rowWidth.length - 1;
|
|
var row = organization.rows[longest];
|
|
var node = row[row.length - 1];
|
|
|
|
var diff = node.width + organization.horizontalPadding;
|
|
|
|
// Check if there is enough space on the last row
|
|
if (organization.width - organization.rowWidth[last] > diff && longest != last) {
|
|
// Remove the last element of the longest row
|
|
row.splice(-1, 1);
|
|
|
|
// Push it to the last row
|
|
organization.rows[last].push(node);
|
|
|
|
organization.rowWidth[longest] = organization.rowWidth[longest] - diff;
|
|
organization.rowWidth[last] = organization.rowWidth[last] + diff;
|
|
organization.width = organization.rowWidth[instance.getLongestRowIndex(organization)];
|
|
|
|
// Update heights of the organization
|
|
var maxHeight = Number.MIN_VALUE;
|
|
for (var i = 0; i < row.length; i++) {
|
|
if (row[i].height > maxHeight) maxHeight = row[i].height;
|
|
}
|
|
if (longest > 0) maxHeight += organization.verticalPadding;
|
|
|
|
var prevTotal = organization.rowHeight[longest] + organization.rowHeight[last];
|
|
|
|
organization.rowHeight[longest] = maxHeight;
|
|
if (organization.rowHeight[last] < node.height + organization.verticalPadding) organization.rowHeight[last] = node.height + organization.verticalPadding;
|
|
|
|
var finalTotal = organization.rowHeight[longest] + organization.rowHeight[last];
|
|
organization.height += finalTotal - prevTotal;
|
|
|
|
this.shiftToLastRow(organization);
|
|
}
|
|
};
|
|
|
|
CoSELayout.prototype.tilingPreLayout = function () {
|
|
if (CoSEConstants.TILE) {
|
|
// Find zero degree nodes and create a compound for each level
|
|
this.groupZeroDegreeMembers();
|
|
// Tile and clear children of each compound
|
|
this.clearCompounds();
|
|
// Separately tile and clear zero degree nodes for each level
|
|
this.clearZeroDegreeMembers();
|
|
}
|
|
};
|
|
|
|
CoSELayout.prototype.tilingPostLayout = function () {
|
|
if (CoSEConstants.TILE) {
|
|
this.repopulateZeroDegreeMembers();
|
|
this.repopulateCompounds();
|
|
}
|
|
};
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Section: Tree Reduction methods
|
|
// -----------------------------------------------------------------------------
|
|
// Reduce trees
|
|
CoSELayout.prototype.reduceTrees = function () {
|
|
var prunedNodesAll = [];
|
|
var containsLeaf = true;
|
|
var node;
|
|
|
|
while (containsLeaf) {
|
|
var allNodes = this.graphManager.getAllNodes();
|
|
var prunedNodesInStepTemp = [];
|
|
containsLeaf = false;
|
|
|
|
for (var i = 0; i < allNodes.length; i++) {
|
|
node = allNodes[i];
|
|
if (node.getEdges().length == 1 && !node.getEdges()[0].isInterGraph && node.getChild() == null) {
|
|
if (CoSEConstants.PURE_INCREMENTAL) {
|
|
var otherEnd = node.getEdges()[0].getOtherEnd(node);
|
|
var relativePosition = new DimensionD(node.getCenterX() - otherEnd.getCenterX(), node.getCenterY() - otherEnd.getCenterY());
|
|
prunedNodesInStepTemp.push([node, node.getEdges()[0], node.getOwner(), relativePosition]);
|
|
} else {
|
|
prunedNodesInStepTemp.push([node, node.getEdges()[0], node.getOwner()]);
|
|
}
|
|
containsLeaf = true;
|
|
}
|
|
}
|
|
if (containsLeaf == true) {
|
|
var prunedNodesInStep = [];
|
|
for (var j = 0; j < prunedNodesInStepTemp.length; j++) {
|
|
if (prunedNodesInStepTemp[j][0].getEdges().length == 1) {
|
|
prunedNodesInStep.push(prunedNodesInStepTemp[j]);
|
|
prunedNodesInStepTemp[j][0].getOwner().remove(prunedNodesInStepTemp[j][0]);
|
|
}
|
|
}
|
|
prunedNodesAll.push(prunedNodesInStep);
|
|
this.graphManager.resetAllNodes();
|
|
this.graphManager.resetAllEdges();
|
|
}
|
|
}
|
|
this.prunedNodesAll = prunedNodesAll;
|
|
};
|
|
|
|
// Grow tree one step
|
|
CoSELayout.prototype.growTree = function (prunedNodesAll) {
|
|
var lengthOfPrunedNodesInStep = prunedNodesAll.length;
|
|
var prunedNodesInStep = prunedNodesAll[lengthOfPrunedNodesInStep - 1];
|
|
|
|
var nodeData;
|
|
for (var i = 0; i < prunedNodesInStep.length; i++) {
|
|
nodeData = prunedNodesInStep[i];
|
|
|
|
this.findPlaceforPrunedNode(nodeData);
|
|
|
|
nodeData[2].add(nodeData[0]);
|
|
nodeData[2].add(nodeData[1], nodeData[1].source, nodeData[1].target);
|
|
}
|
|
|
|
prunedNodesAll.splice(prunedNodesAll.length - 1, 1);
|
|
this.graphManager.resetAllNodes();
|
|
this.graphManager.resetAllEdges();
|
|
};
|
|
|
|
// Find an appropriate position to replace pruned node, this method can be improved
|
|
CoSELayout.prototype.findPlaceforPrunedNode = function (nodeData) {
|
|
|
|
var gridForPrunedNode;
|
|
var nodeToConnect;
|
|
var prunedNode = nodeData[0];
|
|
if (prunedNode == nodeData[1].source) {
|
|
nodeToConnect = nodeData[1].target;
|
|
} else {
|
|
nodeToConnect = nodeData[1].source;
|
|
}
|
|
|
|
if (CoSEConstants.PURE_INCREMENTAL) {
|
|
prunedNode.setCenter(nodeToConnect.getCenterX() + nodeData[3].getWidth(), nodeToConnect.getCenterY() + nodeData[3].getHeight());
|
|
} else {
|
|
var startGridX = nodeToConnect.startX;
|
|
var finishGridX = nodeToConnect.finishX;
|
|
var startGridY = nodeToConnect.startY;
|
|
var finishGridY = nodeToConnect.finishY;
|
|
|
|
var upNodeCount = 0;
|
|
var downNodeCount = 0;
|
|
var rightNodeCount = 0;
|
|
var leftNodeCount = 0;
|
|
var controlRegions = [upNodeCount, rightNodeCount, downNodeCount, leftNodeCount];
|
|
|
|
if (startGridY > 0) {
|
|
for (var i = startGridX; i <= finishGridX; i++) {
|
|
controlRegions[0] += this.grid[i][startGridY - 1].length + this.grid[i][startGridY].length - 1;
|
|
}
|
|
}
|
|
if (finishGridX < this.grid.length - 1) {
|
|
for (var i = startGridY; i <= finishGridY; i++) {
|
|
controlRegions[1] += this.grid[finishGridX + 1][i].length + this.grid[finishGridX][i].length - 1;
|
|
}
|
|
}
|
|
if (finishGridY < this.grid[0].length - 1) {
|
|
for (var i = startGridX; i <= finishGridX; i++) {
|
|
controlRegions[2] += this.grid[i][finishGridY + 1].length + this.grid[i][finishGridY].length - 1;
|
|
}
|
|
}
|
|
if (startGridX > 0) {
|
|
for (var i = startGridY; i <= finishGridY; i++) {
|
|
controlRegions[3] += this.grid[startGridX - 1][i].length + this.grid[startGridX][i].length - 1;
|
|
}
|
|
}
|
|
var min = Integer.MAX_VALUE;
|
|
var minCount;
|
|
var minIndex;
|
|
for (var j = 0; j < controlRegions.length; j++) {
|
|
if (controlRegions[j] < min) {
|
|
min = controlRegions[j];
|
|
minCount = 1;
|
|
minIndex = j;
|
|
} else if (controlRegions[j] == min) {
|
|
minCount++;
|
|
}
|
|
}
|
|
|
|
if (minCount == 3 && min == 0) {
|
|
if (controlRegions[0] == 0 && controlRegions[1] == 0 && controlRegions[2] == 0) {
|
|
gridForPrunedNode = 1;
|
|
} else if (controlRegions[0] == 0 && controlRegions[1] == 0 && controlRegions[3] == 0) {
|
|
gridForPrunedNode = 0;
|
|
} else if (controlRegions[0] == 0 && controlRegions[2] == 0 && controlRegions[3] == 0) {
|
|
gridForPrunedNode = 3;
|
|
} else if (controlRegions[1] == 0 && controlRegions[2] == 0 && controlRegions[3] == 0) {
|
|
gridForPrunedNode = 2;
|
|
}
|
|
} else if (minCount == 2 && min == 0) {
|
|
var random = Math.floor(Math.random() * 2);
|
|
if (controlRegions[0] == 0 && controlRegions[1] == 0) {
|
|
;
|
|
if (random == 0) {
|
|
gridForPrunedNode = 0;
|
|
} else {
|
|
gridForPrunedNode = 1;
|
|
}
|
|
} else if (controlRegions[0] == 0 && controlRegions[2] == 0) {
|
|
if (random == 0) {
|
|
gridForPrunedNode = 0;
|
|
} else {
|
|
gridForPrunedNode = 2;
|
|
}
|
|
} else if (controlRegions[0] == 0 && controlRegions[3] == 0) {
|
|
if (random == 0) {
|
|
gridForPrunedNode = 0;
|
|
} else {
|
|
gridForPrunedNode = 3;
|
|
}
|
|
} else if (controlRegions[1] == 0 && controlRegions[2] == 0) {
|
|
if (random == 0) {
|
|
gridForPrunedNode = 1;
|
|
} else {
|
|
gridForPrunedNode = 2;
|
|
}
|
|
} else if (controlRegions[1] == 0 && controlRegions[3] == 0) {
|
|
if (random == 0) {
|
|
gridForPrunedNode = 1;
|
|
} else {
|
|
gridForPrunedNode = 3;
|
|
}
|
|
} else {
|
|
if (random == 0) {
|
|
gridForPrunedNode = 2;
|
|
} else {
|
|
gridForPrunedNode = 3;
|
|
}
|
|
}
|
|
} else if (minCount == 4 && min == 0) {
|
|
var random = Math.floor(Math.random() * 4);
|
|
gridForPrunedNode = random;
|
|
} else {
|
|
gridForPrunedNode = minIndex;
|
|
}
|
|
|
|
if (gridForPrunedNode == 0) {
|
|
prunedNode.setCenter(nodeToConnect.getCenterX(), nodeToConnect.getCenterY() - nodeToConnect.getHeight() / 2 - FDLayoutConstants.DEFAULT_EDGE_LENGTH - prunedNode.getHeight() / 2);
|
|
} else if (gridForPrunedNode == 1) {
|
|
prunedNode.setCenter(nodeToConnect.getCenterX() + nodeToConnect.getWidth() / 2 + FDLayoutConstants.DEFAULT_EDGE_LENGTH + prunedNode.getWidth() / 2, nodeToConnect.getCenterY());
|
|
} else if (gridForPrunedNode == 2) {
|
|
prunedNode.setCenter(nodeToConnect.getCenterX(), nodeToConnect.getCenterY() + nodeToConnect.getHeight() / 2 + FDLayoutConstants.DEFAULT_EDGE_LENGTH + prunedNode.getHeight() / 2);
|
|
} else {
|
|
prunedNode.setCenter(nodeToConnect.getCenterX() - nodeToConnect.getWidth() / 2 - FDLayoutConstants.DEFAULT_EDGE_LENGTH - prunedNode.getWidth() / 2, nodeToConnect.getCenterY());
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = CoSELayout;
|
|
|
|
/***/ }),
|
|
/* 8 */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
|
|
var coseBase = {};
|
|
|
|
coseBase.layoutBase = __webpack_require__(0);
|
|
coseBase.CoSEConstants = __webpack_require__(1);
|
|
coseBase.CoSEEdge = __webpack_require__(2);
|
|
coseBase.CoSEGraph = __webpack_require__(3);
|
|
coseBase.CoSEGraphManager = __webpack_require__(4);
|
|
coseBase.CoSELayout = __webpack_require__(7);
|
|
coseBase.CoSENode = __webpack_require__(5);
|
|
coseBase.ConstraintHandler = __webpack_require__(6);
|
|
|
|
module.exports = coseBase;
|
|
|
|
/***/ })
|
|
/******/ ]);
|
|
}); |