476 lines
14 KiB
JavaScript
476 lines
14 KiB
JavaScript
/** Decorates with functions required by mermaids dagre-wrapper. */
|
|
import { log } from '../logger.js';
|
|
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
|
|
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
|
|
|
export let clusterDb = {};
|
|
let descendants = {};
|
|
let parents = {};
|
|
|
|
export const clear = () => {
|
|
descendants = {};
|
|
parents = {};
|
|
clusterDb = {};
|
|
};
|
|
|
|
const isDescendant = (id, ancestorId) => {
|
|
// if (id === ancestorId) return true;
|
|
|
|
log.trace('In isDescendant', ancestorId, ' ', id, ' = ', descendants[ancestorId].includes(id));
|
|
if (descendants[ancestorId].includes(id)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const edgeInCluster = (edge, clusterId) => {
|
|
log.info('Descendants of ', clusterId, ' is ', descendants[clusterId]);
|
|
log.info('Edge is ', edge);
|
|
// Edges to/from the cluster is not in the cluster, they are in the parent
|
|
if (edge.v === clusterId) {
|
|
return false;
|
|
}
|
|
if (edge.w === clusterId) {
|
|
return false;
|
|
}
|
|
|
|
if (!descendants[clusterId]) {
|
|
log.debug('Tilt, ', clusterId, ',not in descendants');
|
|
return false;
|
|
}
|
|
return (
|
|
descendants[clusterId].includes(edge.v) ||
|
|
isDescendant(edge.v, clusterId) ||
|
|
isDescendant(edge.w, clusterId) ||
|
|
descendants[clusterId].includes(edge.w)
|
|
);
|
|
};
|
|
|
|
const copy = (clusterId, graph, newGraph, rootId) => {
|
|
log.warn(
|
|
'Copying children of ',
|
|
clusterId,
|
|
'root',
|
|
rootId,
|
|
'data',
|
|
graph.node(clusterId),
|
|
rootId
|
|
);
|
|
const nodes = graph.children(clusterId) || [];
|
|
|
|
// Include cluster node if it is not the root
|
|
if (clusterId !== rootId) {
|
|
nodes.push(clusterId);
|
|
}
|
|
|
|
log.warn('Copying (nodes) clusterId', clusterId, 'nodes', nodes);
|
|
|
|
nodes.forEach((node) => {
|
|
if (graph.children(node).length > 0) {
|
|
copy(node, graph, newGraph, rootId);
|
|
} else {
|
|
const data = graph.node(node);
|
|
log.info('cp ', node, ' to ', rootId, ' with parent ', clusterId); //,node, data, ' parent is ', clusterId);
|
|
newGraph.setNode(node, data);
|
|
if (rootId !== graph.parent(node)) {
|
|
log.warn('Setting parent', node, graph.parent(node));
|
|
newGraph.setParent(node, graph.parent(node));
|
|
}
|
|
|
|
if (clusterId !== rootId && node !== clusterId) {
|
|
log.debug('Setting parent', node, clusterId);
|
|
newGraph.setParent(node, clusterId);
|
|
} else {
|
|
log.info('In copy ', clusterId, 'root', rootId, 'data', graph.node(clusterId), rootId);
|
|
log.debug(
|
|
'Not Setting parent for node=',
|
|
node,
|
|
'cluster!==rootId',
|
|
clusterId !== rootId,
|
|
'node!==clusterId',
|
|
node !== clusterId
|
|
);
|
|
}
|
|
const edges = graph.edges(node);
|
|
log.debug('Copying Edges', edges);
|
|
edges.forEach((edge) => {
|
|
log.info('Edge', edge);
|
|
const data = graph.edge(edge.v, edge.w, edge.name);
|
|
log.info('Edge data', data, rootId);
|
|
try {
|
|
// Do not copy edges in and out of the root cluster, they belong to the parent graph
|
|
if (edgeInCluster(edge, rootId)) {
|
|
log.info('Copying as ', edge.v, edge.w, data, edge.name);
|
|
newGraph.setEdge(edge.v, edge.w, data, edge.name);
|
|
log.info('newGraph edges ', newGraph.edges(), newGraph.edge(newGraph.edges()[0]));
|
|
} else {
|
|
log.info(
|
|
'Skipping copy of edge ',
|
|
edge.v,
|
|
'-->',
|
|
edge.w,
|
|
' rootId: ',
|
|
rootId,
|
|
' clusterId:',
|
|
clusterId
|
|
);
|
|
}
|
|
} catch (e) {
|
|
log.error(e);
|
|
}
|
|
});
|
|
}
|
|
log.debug('Removing node', node);
|
|
graph.removeNode(node);
|
|
});
|
|
};
|
|
export const extractDescendants = (id, graph) => {
|
|
// log.debug('Extracting ', id);
|
|
const children = graph.children(id);
|
|
let res = [...children];
|
|
|
|
for (const child of children) {
|
|
parents[child] = id;
|
|
res = [...res, ...extractDescendants(child, graph)];
|
|
}
|
|
|
|
return res;
|
|
};
|
|
|
|
/**
|
|
* Validates the graph, checking that all parent child relation points to existing nodes and that
|
|
* edges between nodes also ia correct. When not correct the function logs the discrepancies.
|
|
*
|
|
* @param graph
|
|
*/
|
|
export const validate = (graph) => {
|
|
const edges = graph.edges();
|
|
log.trace('Edges: ', edges);
|
|
for (const edge of edges) {
|
|
if (graph.children(edge.v).length > 0) {
|
|
log.trace('The node ', edge.v, ' is part of and edge even though it has children');
|
|
return false;
|
|
}
|
|
if (graph.children(edge.w).length > 0) {
|
|
log.trace('The node ', edge.w, ' is part of and edge even though it has children');
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Finds a child that is not a cluster. When faking an edge between a node and a cluster.
|
|
*
|
|
* @param id
|
|
* @param {any} graph
|
|
*/
|
|
export const findNonClusterChild = (id, graph) => {
|
|
// const node = graph.node(id);
|
|
log.trace('Searching', id);
|
|
// const children = graph.children(id).reverse();
|
|
const children = graph.children(id); //.reverse();
|
|
log.trace('Searching children of id ', id, children);
|
|
if (children.length < 1) {
|
|
log.trace('This is a valid node', id);
|
|
return id;
|
|
}
|
|
for (const child of children) {
|
|
const _id = findNonClusterChild(child, graph);
|
|
if (_id) {
|
|
log.trace('Found replacement for', id, ' => ', _id);
|
|
return _id;
|
|
}
|
|
}
|
|
};
|
|
|
|
const getAnchorId = (id) => {
|
|
if (!clusterDb[id]) {
|
|
return id;
|
|
}
|
|
// If the cluster has no external connections
|
|
if (!clusterDb[id].externalConnections) {
|
|
return id;
|
|
}
|
|
|
|
// Return the replacement node
|
|
if (clusterDb[id]) {
|
|
return clusterDb[id].id;
|
|
}
|
|
return id;
|
|
};
|
|
|
|
export const adjustClustersAndEdges = (graph, depth) => {
|
|
if (!graph || depth > 10) {
|
|
log.debug('Opting out, no graph ');
|
|
return;
|
|
} else {
|
|
log.debug('Opting in, graph ');
|
|
}
|
|
// Go through the nodes and for each cluster found, save a replacement node, this can be used when
|
|
// faking a link to a cluster
|
|
graph.nodes().forEach(function (id) {
|
|
const children = graph.children(id);
|
|
if (children.length > 0) {
|
|
log.warn(
|
|
'Cluster identified',
|
|
id,
|
|
' Replacement id in edges: ',
|
|
findNonClusterChild(id, graph)
|
|
);
|
|
descendants[id] = extractDescendants(id, graph);
|
|
clusterDb[id] = { id: findNonClusterChild(id, graph), clusterData: graph.node(id) };
|
|
}
|
|
});
|
|
|
|
// Check incoming and outgoing edges for each cluster
|
|
graph.nodes().forEach(function (id) {
|
|
const children = graph.children(id);
|
|
const edges = graph.edges();
|
|
if (children.length > 0) {
|
|
log.debug('Cluster identified', id, descendants);
|
|
edges.forEach((edge) => {
|
|
// log.debug('Edge, descendants: ', edge, descendants[id]);
|
|
|
|
// Check if any edge leaves the cluster (not the actual cluster, that's a link from the box)
|
|
if (edge.v !== id && edge.w !== id) {
|
|
// Any edge where either the one of the nodes is descending to the cluster but not the other
|
|
// if (descendants[id].indexOf(edge.v) < 0 && descendants[id].indexOf(edge.w) < 0) {
|
|
|
|
const d1 = isDescendant(edge.v, id);
|
|
const d2 = isDescendant(edge.w, id);
|
|
|
|
// d1 xor d2 - if either d1 is true and d2 is false or the other way around
|
|
if (d1 ^ d2) {
|
|
log.warn('Edge: ', edge, ' leaves cluster ', id);
|
|
log.warn('Descendants of XXX ', id, ': ', descendants[id]);
|
|
clusterDb[id].externalConnections = true;
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
log.debug('Not a cluster ', id, descendants);
|
|
}
|
|
});
|
|
|
|
for (let id of Object.keys(clusterDb)) {
|
|
const nonClusterChild = clusterDb[id].id;
|
|
const parent = graph.parent(nonClusterChild);
|
|
|
|
// Change replacement node of id to parent of current replacement node if valid
|
|
if (parent !== id && clusterDb[parent] && !clusterDb[parent].externalConnections) {
|
|
clusterDb[id].id = parent;
|
|
}
|
|
}
|
|
|
|
// For clusters with incoming and/or outgoing edges translate those edges to a real node
|
|
// in the cluster in order to fake the edge
|
|
graph.edges().forEach(function (e) {
|
|
const edge = graph.edge(e);
|
|
log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
|
log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
|
|
|
|
let v = e.v;
|
|
let w = e.w;
|
|
// Check if link is either from or to a cluster
|
|
log.warn(
|
|
'Fix XXX',
|
|
clusterDb,
|
|
'ids:',
|
|
e.v,
|
|
e.w,
|
|
'Translating: ',
|
|
clusterDb[e.v],
|
|
' --- ',
|
|
clusterDb[e.w]
|
|
);
|
|
if (clusterDb[e.v] && clusterDb[e.w] && clusterDb[e.v] === clusterDb[e.w]) {
|
|
// cspell:ignore trixing
|
|
log.warn('Fixing and trixing link to self - removing XXX', e.v, e.w, e.name);
|
|
log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name);
|
|
v = getAnchorId(e.v);
|
|
w = getAnchorId(e.w);
|
|
graph.removeEdge(e.v, e.w, e.name);
|
|
const specialId = e.w + '---' + e.v;
|
|
graph.setNode(specialId, {
|
|
domId: specialId,
|
|
id: specialId,
|
|
labelStyle: '',
|
|
labelText: edge.label,
|
|
padding: 0,
|
|
shape: 'labelRect',
|
|
style: '',
|
|
});
|
|
const edge1 = structuredClone(edge);
|
|
const edge2 = structuredClone(edge);
|
|
edge1.label = '';
|
|
edge1.arrowTypeEnd = 'none';
|
|
edge2.label = '';
|
|
edge1.fromCluster = e.v;
|
|
edge2.toCluster = e.v;
|
|
|
|
graph.setEdge(v, specialId, edge1, e.name + '-cyclic-special');
|
|
graph.setEdge(specialId, w, edge2, e.name + '-cyclic-special');
|
|
} else if (clusterDb[e.v] || clusterDb[e.w]) {
|
|
log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name);
|
|
v = getAnchorId(e.v);
|
|
w = getAnchorId(e.w);
|
|
graph.removeEdge(e.v, e.w, e.name);
|
|
if (v !== e.v) {
|
|
const parent = graph.parent(v);
|
|
clusterDb[parent].externalConnections = true;
|
|
edge.fromCluster = e.v;
|
|
}
|
|
if (w !== e.w) {
|
|
const parent = graph.parent(w);
|
|
clusterDb[parent].externalConnections = true;
|
|
edge.toCluster = e.w;
|
|
}
|
|
log.warn('Fix Replacing with XXX', v, w, e.name);
|
|
graph.setEdge(v, w, edge, e.name);
|
|
}
|
|
});
|
|
log.warn('Adjusted Graph', graphlibJson.write(graph));
|
|
extractor(graph, 0);
|
|
|
|
log.trace(clusterDb);
|
|
|
|
// Remove references to extracted cluster
|
|
// graph.edges().forEach(edge => {
|
|
// if (isDescendant(edge.v, clusterId) || isDescendant(edge.w, clusterId)) {
|
|
// graph.removeEdge(edge);
|
|
// }
|
|
// });
|
|
};
|
|
|
|
export const extractor = (graph, depth) => {
|
|
log.warn('extractor - ', depth, graphlibJson.write(graph), graph.children('D'));
|
|
if (depth > 10) {
|
|
log.error('Bailing out');
|
|
return;
|
|
}
|
|
// For clusters without incoming and/or outgoing edges, create a new cluster-node
|
|
// containing the nodes and edges in the custer in a new graph
|
|
// for (let i = 0;)
|
|
let nodes = graph.nodes();
|
|
let hasChildren = false;
|
|
for (const node of nodes) {
|
|
const children = graph.children(node);
|
|
hasChildren = hasChildren || children.length > 0;
|
|
}
|
|
|
|
if (!hasChildren) {
|
|
log.debug('Done, no node has children', graph.nodes());
|
|
return;
|
|
}
|
|
// const clusters = Object.keys(clusterDb);
|
|
// clusters.forEach(clusterId => {
|
|
log.debug('Nodes = ', nodes, depth);
|
|
for (const node of nodes) {
|
|
log.debug(
|
|
'Extracting node',
|
|
node,
|
|
clusterDb,
|
|
clusterDb[node] && !clusterDb[node].externalConnections,
|
|
!graph.parent(node),
|
|
graph.node(node),
|
|
graph.children('D'),
|
|
' Depth ',
|
|
depth
|
|
);
|
|
// Note that the node might have been removed after the Object.keys call so better check
|
|
// that it still is in the game
|
|
if (!clusterDb[node]) {
|
|
// Skip if the node is not a cluster
|
|
log.debug('Not a cluster', node, depth);
|
|
// break;
|
|
} else if (
|
|
!clusterDb[node].externalConnections &&
|
|
// !graph.parent(node) &&
|
|
graph.children(node) &&
|
|
graph.children(node).length > 0
|
|
) {
|
|
log.warn(
|
|
'Cluster without external connections, without a parent and with children',
|
|
node,
|
|
depth
|
|
);
|
|
|
|
const graphSettings = graph.graph();
|
|
let dir = graphSettings.rankdir === 'TB' ? 'LR' : 'TB';
|
|
if (clusterDb[node] && clusterDb[node].clusterData && clusterDb[node].clusterData.dir) {
|
|
dir = clusterDb[node].clusterData.dir;
|
|
log.warn('Fixing dir', clusterDb[node].clusterData.dir, dir);
|
|
}
|
|
|
|
const clusterGraph = new graphlib.Graph({
|
|
multigraph: true,
|
|
compound: true,
|
|
})
|
|
.setGraph({
|
|
rankdir: dir, // Todo: set proper spacing
|
|
nodesep: 50,
|
|
ranksep: 50,
|
|
marginx: 8,
|
|
marginy: 8,
|
|
})
|
|
.setDefaultEdgeLabel(function () {
|
|
return {};
|
|
});
|
|
|
|
log.warn('Old graph before copy', graphlibJson.write(graph));
|
|
copy(node, graph, clusterGraph, node);
|
|
graph.setNode(node, {
|
|
clusterNode: true,
|
|
id: node,
|
|
clusterData: clusterDb[node].clusterData,
|
|
labelText: clusterDb[node].labelText,
|
|
graph: clusterGraph,
|
|
});
|
|
log.warn('New graph after copy node: (', node, ')', graphlibJson.write(clusterGraph));
|
|
log.debug('Old graph after copy', graphlibJson.write(graph));
|
|
} else {
|
|
log.warn(
|
|
'Cluster ** ',
|
|
node,
|
|
' **not meeting the criteria !externalConnections:',
|
|
!clusterDb[node].externalConnections,
|
|
' no parent: ',
|
|
!graph.parent(node),
|
|
' children ',
|
|
graph.children(node) && graph.children(node).length > 0,
|
|
graph.children('D'),
|
|
depth
|
|
);
|
|
log.debug(clusterDb);
|
|
}
|
|
}
|
|
|
|
nodes = graph.nodes();
|
|
log.warn('New list of nodes', nodes);
|
|
for (const node of nodes) {
|
|
const data = graph.node(node);
|
|
log.warn(' Now next level', node, data);
|
|
if (data.clusterNode) {
|
|
extractor(data.graph, depth + 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
const sorter = (graph, nodes) => {
|
|
if (nodes.length === 0) {
|
|
return [];
|
|
}
|
|
let result = Object.assign(nodes);
|
|
nodes.forEach((node) => {
|
|
const children = graph.children(node);
|
|
const sorted = sorter(graph, children);
|
|
result = [...result, ...sorted];
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
export const sortNodesByHierarchy = (graph) => sorter(graph, graph.children());
|