import { __awaiter } from "tslib";
import { Graph as GraphCore } from '@antv/graphlib';
import { isFunction, isNumber, isObject } from '@antv/util';
import { ConcentricLayout } from './concentric';
import { ForceLayout } from './force';
import { MDSLayout } from './mds';
import { isLayoutWithIterations } from './types';
import { getLayoutBBox, graphTreeDfs, isArray } from './util';
import { handleSingleNodeGraph } from './util/common';
const FORCE_LAYOUT_TYPE_MAP = {
gForce: true,
force2: true,
d3force: true,
fruchterman: true,
forceAtlas2: true,
force: true,
'graphin-force': true,
};
const DEFAULTS_LAYOUT_OPTIONS = {
center: [0, 0],
comboPadding: 10,
treeKey: 'combo',
};
/**
* 组合布局
*
* Combo-Combined layout
*/
export class ComboCombinedLayout {
constructor(options = {}) {
this.options = options;
this.id = 'comboCombined';
this.options = Object.assign(Object.assign({}, DEFAULTS_LAYOUT_OPTIONS), options);
}
/**
* Return the positions of nodes and edges(if needed).
*/
execute(graph, options) {
return __awaiter(this, void 0, void 0, function* () {
return this.genericComboCombinedLayout(false, graph, options);
});
}
/**
* To directly assign the positions to the nodes.
*/
assign(graph, options) {
return __awaiter(this, void 0, void 0, function* () {
yield this.genericComboCombinedLayout(true, graph, options);
});
}
genericComboCombinedLayout(assign, graph, options) {
return __awaiter(this, void 0, void 0, function* () {
const mergedOptions = this.initVals(Object.assign(Object.assign({}, this.options), options));
const { center, treeKey, outerLayout: propsOuterLayout } = mergedOptions;
const nodes = graph
.getAllNodes()
.filter((node) => !node.data._isCombo);
const combos = graph
.getAllNodes()
.filter((node) => node.data._isCombo);
const edges = graph.getAllEdges();
const n = nodes === null || nodes === void 0 ? void 0 : nodes.length;
if (!n || n === 1) {
return handleSingleNodeGraph(graph, assign, center);
}
// output nodes
const layoutNodes = [];
const nodeMap = new Map();
nodes.forEach((node) => {
nodeMap.set(node.id, node);
});
const comboMap = new Map();
combos.forEach((combo) => {
comboMap.set(combo.id, combo);
});
// each one in comboNodes is a combo contains the size and child nodes
// comboNodes includes the node who has no parent combo
const comboNodes = new Map();
// the inner layouts, the result positions are stored in comboNodes and their child nodes
const innerGraphLayoutPromises = this.getInnerGraphs(graph, treeKey, nodeMap, comboMap, edges, mergedOptions, comboNodes);
yield Promise.all(innerGraphLayoutPromises);
const outerNodeIds = new Map();
const outerLayoutNodes = [];
const nodeAncestorIdMap = new Map();
let allHaveNoPosition = true;
graph.getRoots(treeKey).forEach((root) => {
const combo = comboNodes.get(root.id);
const cacheCombo = comboMap.get(root.id) || nodeMap.get(root.id);
const comboLayoutNode = {
id: root.id,
data: Object.assign(Object.assign({}, root.data), { x: combo.data.x || cacheCombo.data.x, y: combo.data.y || cacheCombo.data.y, fx: combo.data.fx || cacheCombo.data.fx, fy: combo.data.fy || cacheCombo.data.fy, mass: combo.data.mass || cacheCombo.data.mass, size: combo.data.size }),
};
outerLayoutNodes.push(comboLayoutNode);
outerNodeIds.set(root.id, true);
if (!isNaN(comboLayoutNode.data.x) &&
comboLayoutNode.data.x !== 0 &&
!isNaN(comboLayoutNode.data.y) &&
comboLayoutNode.data.y !== 0) {
allHaveNoPosition = false;
}
else {
comboLayoutNode.data.x = Math.random() * 100;
comboLayoutNode.data.y = Math.random() * 100;
}
graphTreeDfs(graph, [root], (child) => {
if (child.id !== root.id)
nodeAncestorIdMap.set(child.id, root.id);
}, 'TB', treeKey);
});
const outerLayoutEdges = [];
edges.forEach((edge) => {
const sourceAncestorId = nodeAncestorIdMap.get(edge.source) || edge.source;
const targetAncestorId = nodeAncestorIdMap.get(edge.target) || edge.target;
// create an edge for outer layout if both source and target's ancestor combo is in outer layout nodes
if (sourceAncestorId !== targetAncestorId &&
outerNodeIds.has(sourceAncestorId) &&
outerNodeIds.has(targetAncestorId)) {
outerLayoutEdges.push({
id: edge.id,
source: sourceAncestorId,
target: targetAncestorId,
data: {},
});
}
});
// 若有需要最外层的 combo 或节点,则对最外层执行力导向
let outerPositions;
if (outerLayoutNodes === null || outerLayoutNodes === void 0 ? void 0 : outerLayoutNodes.length) {
if (outerLayoutNodes.length === 1) {
outerLayoutNodes[0].data.x = center[0];
outerLayoutNodes[0].data.y = center[1];
}
else {
const outerLayoutGraph = new GraphCore({
nodes: outerLayoutNodes,
edges: outerLayoutEdges,
});
const outerLayout = propsOuterLayout || new ForceLayout();
// preset the nodes if the outerLayout is a force family layout
if (allHaveNoPosition && FORCE_LAYOUT_TYPE_MAP[outerLayout.id]) {
const outerLayoutPreset = outerLayoutNodes.length < 100
? new MDSLayout()
: new ConcentricLayout();
yield outerLayoutPreset.assign(outerLayoutGraph);
}
const options = Object.assign({ center, kg: 5, preventOverlap: true, animate: false }, (outerLayout.id === 'force'
? {
gravity: 1,
factor: 4,
linkDistance: (edge, source, target) => {
const sourceSize = Math.max(...source.data.size) || 32;
const targetSize = Math.max(...target.data.size) || 32;
return sourceSize / 2 + targetSize / 2 + 200;
},
}
: {}));
outerPositions = yield executeLayout(outerLayout, outerLayoutGraph, options);
}
// move the combos and their child nodes
comboNodes.forEach((comboNode) => {
var _a;
const outerPosition = outerPositions.nodes.find((pos) => pos.id === comboNode.id);
if (outerPosition) {
// if it is one of the outer layout nodes, update the positions
const { x, y } = outerPosition.data;
comboNode.data.visited = true;
comboNode.data.x = x;
comboNode.data.y = y;
layoutNodes.push({
id: comboNode.id,
data: { x, y },
});
}
// move the child nodes
const { x, y } = comboNode.data;
(_a = comboNode.data.nodes) === null || _a === void 0 ? void 0 : _a.forEach((node) => {
layoutNodes.push({
id: node.id,
data: { x: node.data.x + x, y: node.data.y + y },
});
});
});
// move the nodes from top to bottom
comboNodes.forEach(({ data }) => {
const { x, y, visited, nodes } = data;
nodes === null || nodes === void 0 ? void 0 : nodes.forEach((node) => {
if (!visited) {
const layoutNode = layoutNodes.find((n) => n.id === node.id);
layoutNode.data.x += x || 0;
layoutNode.data.y += y || 0;
}
});
});
}
if (assign) {
layoutNodes.forEach((node) => {
graph.mergeNodeData(node.id, {
x: node.data.x,
y: node.data.y,
});
});
}
const result = {
nodes: layoutNodes,
edges,
};
return result;
});
}
initVals(options) {
const formattedOptions = Object.assign({}, options);
const { nodeSize, spacing, comboPadding } = options;
let nodeSizeFunc;
let spacingFunc;
// nodeSpacing to function
if (isNumber(spacing)) {
spacingFunc = () => spacing;
}
else if (isFunction(spacing)) {
spacingFunc = spacing;
}
else {
spacingFunc = () => 0;
}
formattedOptions.spacing = spacingFunc;
// nodeSize to function
if (!nodeSize) {
nodeSizeFunc = (d) => {
const spacing = spacingFunc(d);
if (d.size) {
if (isArray(d.size)) {
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
return (res + spacing) / 2;
}
if (isObject(d.size)) {
const res = d.size.width > d.size.height ? d.size.width : d.size.height;
return (res + spacing) / 2;
}
return (d.size + spacing) / 2;
}
return 32 + spacing / 2;
};
}
else if (isFunction(nodeSize)) {
nodeSizeFunc = (d) => {
const size = nodeSize(d);
const spacing = spacingFunc(d);
if (isArray(d.size)) {
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
return (res + spacing) / 2;
}
return ((size || 32) + spacing) / 2;
};
}
else if (isArray(nodeSize)) {
const larger = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
const radius = larger / 2;
nodeSizeFunc = (d) => radius + spacingFunc(d) / 2;
}
else {
// number type
const radius = nodeSize / 2;
nodeSizeFunc = (d) => radius + spacingFunc(d) / 2;
}
formattedOptions.nodeSize = nodeSizeFunc;
// comboPadding to function
let comboPaddingFunc;
if (isNumber(comboPadding)) {
comboPaddingFunc = () => comboPadding;
}
else if (isArray(comboPadding)) {
comboPaddingFunc = () => Math.max.apply(null, comboPadding);
}
else if (isFunction(comboPadding)) {
comboPaddingFunc = comboPadding;
}
else {
// null type
comboPaddingFunc = () => 0;
}
formattedOptions.comboPadding = comboPaddingFunc;
return formattedOptions;
}
getInnerGraphs(graph, treeKey, nodeMap, comboMap, edges, options, comboNodes) {
const { nodeSize, comboPadding, spacing, innerLayout } = options;
const innerGraphLayout = innerLayout || new ConcentricLayout({});
const innerLayoutOptions = {
center: [0, 0],
preventOverlap: true,
nodeSpacing: spacing,
};
const innerLayoutPromises = [];
const getSize = (node) => {
// @ts-ignore
let padding = (comboPadding === null || comboPadding === void 0 ? void 0 : comboPadding(node)) || 10;
if (isArray(padding))
padding = Math.max(...padding);
return {
size: padding ? [padding * 2, padding * 2] : [30, 30],
padding,
};
};
graph.getRoots(treeKey).forEach((root) => {
// @ts-ignore
comboNodes.set(root.id, {
id: root.id,
data: {
nodes: [],
size: getSize(root).size,
},
});
let start = Promise.resolve();
// Regard the child nodes in one combo as a graph, and layout them from bottom to top
graphTreeDfs(graph, [root], (treeNode) => {
var _a;
if (!treeNode.data._isCombo)
return;
const { size: nsize, padding } = getSize(treeNode);
if (!((_a = graph.getChildren(treeNode.id, treeKey)) === null || _a === void 0 ? void 0 : _a.length)) {
// empty combo
comboNodes.set(treeNode.id, {
id: treeNode.id,
data: Object.assign(Object.assign({}, treeNode.data), { size: nsize }),
});
}
else {
// combo not empty
const comboNode = comboNodes.get(treeNode.id);
comboNodes.set(treeNode.id, {
id: treeNode.id,
data: Object.assign({ nodes: [] }, comboNode === null || comboNode === void 0 ? void 0 : comboNode.data),
});
const innerLayoutNodeIds = new Map();
const innerLayoutNodes = graph
.getChildren(treeNode.id, treeKey)
.map((child) => {
if (child.data._isCombo) {
if (!comboNodes.has(child.id)) {
comboNodes.set(child.id, {
id: child.id,
data: Object.assign({}, child.data),
});
}
innerLayoutNodeIds.set(child.id, true);
return comboNodes.get(child.id);
}
const oriNode = nodeMap.get(child.id) || comboMap.get(child.id);
innerLayoutNodeIds.set(child.id, true);
return {
id: child.id,
data: Object.assign(Object.assign({}, oriNode.data), child.data),
};
});
const innerGraphData = {
nodes: innerLayoutNodes,
edges: edges.filter((edge) => innerLayoutNodeIds.has(edge.source) &&
innerLayoutNodeIds.has(edge.target)),
};
let minNodeSize = Infinity;
innerLayoutNodes.forEach((node) => {
var _a;
let { size } = node.data;
if (!size) {
size = ((_a = comboNodes.get(node.id)) === null || _a === void 0 ? void 0 : _a.data.size) ||
(nodeSize === null || nodeSize === void 0 ? void 0 : nodeSize(node)) || [30, 30];
}
if (isNumber(size))
size = [size, size];
const [size0, size1] = size;
if (minNodeSize > size0)
minNodeSize = size0;
if (minNodeSize > size1)
minNodeSize = size1;
node.data.size = size;
});
// innerGraphLayout.assign(innerGraphCore, innerLayoutOptions);
start = start.then(() => __awaiter(this, void 0, void 0, function* () {
const innerGraphCore = new GraphCore(innerGraphData);
yield executeLayout(innerGraphLayout, innerGraphCore, innerLayoutOptions, true);
const { minX, minY, maxX, maxY } = getLayoutBBox(innerLayoutNodes);
// move the innerGraph to [0, 0], for later controlled by parent layout
const center = { x: (maxX + minX) / 2, y: (maxY + minY) / 2 };
innerGraphData.nodes.forEach((node) => {
node.data.x -= center.x;
node.data.y -= center.y;
});
const size = [
Math.max(maxX - minX, minNodeSize) + padding * 2,
Math.max(maxY - minY, minNodeSize) + padding * 2,
];
comboNodes.get(treeNode.id).data.size = size;
comboNodes.get(treeNode.id).data.nodes = innerLayoutNodes;
}));
}
return true;
}, 'BT', treeKey);
innerLayoutPromises.push(start);
});
return innerLayoutPromises;
}
}
function executeLayout(layout, graph, options, assign) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (isLayoutWithIterations(layout)) {
layout.execute(graph, options);
layout.stop();
return layout.tick((_a = options.iterations) !== null && _a !== void 0 ? _a : 300);
}
if (assign)
return yield layout.assign(graph, options);
return yield layout.execute(graph, options);
});
}
//# sourceMappingURL=combo-combined.js.map