blob: e27dfb3648d60b5bd43a8f71a8c9a01ac846f1b1 [file] [log] [blame]
library graphlib.layout;
import "dart:math" as Math;
import "acyclic.dart" as acyclic;
import "normalize.dart" as normalize;
import "rank/rank.dart";
import "parent_dummy_chains.dart";
import "util.dart" show removeEmptyRanks, normalizeRanks;
import "nesting_graph.dart" as nestingGraph;
import "add_border_segments.dart";
import "coordinate_system.dart" as coordinateSystem;
import "order/order.dart";
import "position/position.dart";
import "util.dart" as util;
import "../graph.dart" show Graph;
layout(Graph g, [bool debugTiming=false]) {
var time = debugTiming ? util.time : util.notime;
time("layout", () {
var layoutGraph = time(" buildLayoutGraph",
() { return buildLayoutGraph(g); });
time(" runLayout", () { runLayout(layoutGraph, time); });
time(" updateInputGraph", () { updateInputGraph(g, layoutGraph); });
});
}
runLayout(Graph g, time) {
time(" makeSpaceForEdgeLabels", () { makeSpaceForEdgeLabels(g); });
time(" removeSelfEdges", () { removeSelfEdges(g); });
time(" acyclic", () { acyclic.run(g); });
time(" nestingGraph.run", () { nestingGraph.run(g); });
time(" rank", () { rank(util.asNonCompoundGraph(g)); });
time(" injectEdgeLabelProxies", () { injectEdgeLabelProxies(g); });
time(" removeEmptyRanks", () { removeEmptyRanks(g); });
time(" nestingGraph.cleanup", () { nestingGraph.cleanup(g); });
time(" normalizeRanks", () { normalizeRanks(g); });
time(" assignRankMinMax", () { assignRankMinMax(g); });
time(" removeEdgeLabelProxies", () { removeEdgeLabelProxies(g); });
time(" normalize.run", () { normalize.run(g); });
time(" parentDummyChains", () { parentDummyChains(g); });
time(" addBorderSegments", () { addBorderSegments(g); });
time(" order", () { order(g); });
time(" insertSelfEdges", () { insertSelfEdges(g); });
time(" adjustCoordinateSystem", () { coordinateSystem.adjust(g); });
time(" position", () { position(g); });
time(" positionSelfEdges", () { positionSelfEdges(g); });
time(" removeBorderNodes", () { removeBorderNodes(g); });
time(" normalize.undo", () { normalize.undo(g); });
time(" fixupEdgeLabelCoords", () { fixupEdgeLabelCoords(g); });
time(" undoCoordinateSystem", () { coordinateSystem.undo(g); });
time(" translateGraph", () { translateGraph(g); });
time(" assignNodeIntersects", () { assignNodeIntersects(g); });
time(" reversePoints", () { reversePointsForReversedEdges(g); });
time(" acyclic.undo", () { acyclic.undo(g); });
}
/*
* Copies final layout information from the layout graph back to the input
* graph. This process only copies whitelisted attributes from the layout graph
* to the input graph, so it serves as a good place to determine what
* attributes can influence layout.
*/
updateInputGraph(Graph inputGraph, Graph layoutGraph) {
inputGraph.nodes.forEach((v) {
Map inputLabel = inputGraph.node(v),
layoutLabel = layoutGraph.node(v);
if (inputLabel != null) {
inputLabel["x"] = layoutLabel["x"];
inputLabel["y"] = layoutLabel["y"];
if (layoutGraph.children(v).length != 0) {
inputLabel["width"] = layoutLabel["width"];
inputLabel["height"] = layoutLabel["height"];
}
}
});
inputGraph.edges.forEach((e) {
Map inputLabel = inputGraph.edgeObj(e),
layoutLabel = layoutGraph.edgeObj(e);
inputLabel["points"] = layoutLabel["points"];
if (layoutLabel.containsKey("x")) {
inputLabel["x"] = layoutLabel["x"];
inputLabel["y"] = layoutLabel["y"];
}
});
inputGraph.graph()["width"] = layoutGraph.graph()["width"];
inputGraph.graph()["height"] = layoutGraph.graph()["height"];
}
const graphNumAttrs = const ["nodesep", "edgesep", "ranksep", "marginx", "marginy"],
graphDefaults = const { "ranksep": 50, "edgesep": 20, "nodesep": 50, "rankdir": "tb" },
graphAttrs = const ["acyclicer", "ranker", "rankdir", "align"],
nodeNumAttrs = const ["width", "height"],
nodeDefaults = const { "width": 0, "height": 0 },
edgeNumAttrs = const ["minlen", "weight", "width", "height", "labeloffset"],
edgeDefaults = const {
"minlen": 1, "weight": 1, "width": 0, "height": 0,
"labeloffset": 10, "labelpos": "r"
},
edgeAttrs = const ["labelpos"];
/// Constructs a new graph from the input graph, which can be used for layout.
/// This process copies only whitelisted attributes from the input graph to the
/// layout graph. Thus this function serves as a good place to determine what
/// attributes can influence layout.
buildLayoutGraph(Graph inputGraph) {
var g = new Graph(multigraph: true, compound: true),
graph = canonicalize(inputGraph.graph());
g.setGraph(util.merge({},
[graphDefaults,
selectNumberAttrs(graph, graphNumAttrs),
util.pick(graph, graphAttrs)]));
inputGraph.nodes.forEach((v) {
var node = canonicalize(inputGraph.node(v));
g.setNode(v, util.defaults(selectNumberAttrs(node, nodeNumAttrs), nodeDefaults));
g.setParent(v, inputGraph.parent(v));
});
inputGraph.edges.forEach((e) {
var edge = canonicalize(inputGraph.edgeObj(e));
g.setEdge(e, util.merge({},
[edgeDefaults,
selectNumberAttrs(edge, edgeNumAttrs),
util.pick(edge, edgeAttrs)]));
});
return g;
}
/// This idea comes from the Gansner paper: to account for edge labels in our
/// layout we split each rank in half by doubling minlen and halving ranksep.
/// Then we can place labels at these mid-points between nodes.
///
/// We also add some minimal padding to the width to push the label for the edge
/// away from the edge itself a bit.
makeSpaceForEdgeLabels(Graph g) {
var graph = g.graph();
graph["ranksep"] /= 2;
g.edges.forEach((e) {
Map edge = g.edgeObj(e);
edge["minlen"] *= 2;
if (edge["labelpos"].toLowerCase() != "c") {
if (graph["rankdir"] == "TB" || graph["rankdir"] == "BT") {
edge["width"] += edge["labeloffset"];
} else {
edge["height"] += edge["labeloffset"];
}
}
});
}
/// Creates temporary dummy nodes that capture the rank in which each edge's
/// label is going to, if it has one of non-zero width and height. We do this
/// so that we can safely remove empty ranks while preserving balance for the
/// label's position.
injectEdgeLabelProxies(Graph g) {
g.edges.forEach((e) {
Map edge = g.edgeObj(e);
if (edge["width"] && edge["height"]) {
Map v = g.node(e.v),
w = g.node(e.w),
label = { "rank": (w["rank"] - v["rank"]) / 2 + v["rank"], "e": e };
util.addDummyNode(g, "edge-proxy", label, "_ep");
}
});
}
assignRankMinMax(Graph g) {
var maxRank = 0;
g.nodes.forEach((v) {
Map node = g.node(v);
if (node["borderTop"]) {
node["minRank"] = g.node(node["borderTop"])["rank"];
node["maxRank"] = g.node(node["borderBottom"])["rank"];
maxRank = Math.max(maxRank, node["maxRank"]);
}
});
g.graph()["maxRank"] = maxRank;
}
removeEdgeLabelProxies(Graph g) {
g.nodes.forEach((v) {
Map node = g.node(v);
if (node["dummy"] == "edge-proxy") {
g.edgeObj(node["e"])["labelRank"] = node["rank"];
g.removeNode(v);
}
});
}
translateGraph(Graph g) {
var minX = double.INFINITY,
maxX = 0,
minY = double.INFINITY,
maxY = 0,
graphLabel = g.graph(),
marginX = graphLabel["marginx"],
marginY = graphLabel["marginy"];
if (marginX == null) {
marginX = 0;
}
if (marginY == null) {
marginY = 0;
}
getExtremes(Map attrs) {
var x = attrs["x"],
y = attrs["y"],
w = attrs["width"],
h = attrs["height"];
minX = Math.min(minX, x - w / 2);
maxX = Math.max(maxX, x + w / 2);
minY = Math.min(minY, y - h / 2);
maxY = Math.max(maxY, y + h / 2);
}
g.nodes.forEach((v) { getExtremes(g.node(v)); });
g.edges.forEach((e) {
Map edge = g.edgeObj(e);
if (edge.containsKey("x")) {
getExtremes(edge);
}
});
minX -= marginX;
minY -= marginY;
g.nodes.forEach((v) {
Map node = g.node(v);
node["x"] -= minX;
node["y"] -= minY;
});
g.edges.forEach((e) {
Map edge = g.edgeObj(e);
edge["points"].forEach((p) {
p.x -= minX;
p.y -= minY;
});
if (edge.containsKey("x")) { edge["x"] -= minX; }
if (edge.containsKey("y")) { edge["y"] -= minY; }
});
graphLabel["width"] = maxX - minX + marginX;
graphLabel["height"] = maxY - minY + marginY;
}
assignNodeIntersects(Graph g) {
g.edges.forEach((e) {
Map edge = g.edgeObj(e),
nodeV = g.node(e.v),
nodeW = g.node(e.w),
p1, p2;
if (edge["points"] == null) {
edge["points"] = [];
p1 = nodeW;
p2 = nodeV;
} else {
p1 = edge["points"][0];
p2 = edge["points"][edge["points"].length - 1];
}
edge["points"].insert(0, util.intersectRect(nodeV, p1));
edge["points"].add(util.intersectRect(nodeW, p2));
});
}
fixupEdgeLabelCoords(Graph g) {
g.edges.forEach((e) {
Map edge = g.edgeObj(e);
if (edge.containsKey("x")) {
if (edge["labelpos"] == "l" || edge["labelpos"] == "r") {
edge["width"] -= edge["labeloffset"];
}
switch (edge["labelpos"]) {
case "l": edge["x"] -= edge["width"] / 2 + edge["labeloffset"]; break;
case "r": edge["x"] += edge["width"] / 2 + edge["labeloffset"]; break;
}
}
});
}
reversePointsForReversedEdges(Graph g) {
g.edges.forEach((e) {
Map edge = g.edgeObj(e);
if (edge["reversed"]) {
edge["points"].reverse();
}
});
}
removeBorderNodes(Graph g) {
g.nodes.forEach((v) {
if (g.children(v).length != 0) {
Map node = g.node(v),
t = g.node(node["borderTop"]),
b = g.node(node["borderBottom"]),
l = g.node(node["borderLeft"].last),
r = g.node(node["borderRight"].last);
node["width"] = (r["x"] - l["x"]).abs();
node["height"] = (b["y"] - t["y"]).abs();
node["x"] = l["x"] + node["width"] / 2;
node["y"] = t["y"] + node["height"] / 2;
}
});
g.nodes.forEach((v) {
if (g.node(v)["dummy"] == "border") {
g.removeNode(v);
}
});
}
removeSelfEdges(Graph g) {
g.edges.forEach((e) {
if (e.v == e.w) {
Map node = g.node(e.v);
if (!node["selfEdges"]) {
node["selfEdges"] = [];
}
node["selfEdges"].add({ "e": e, "label": g.edgeObj(e) });
g.removeEdgeObj(e);
}
});
}
insertSelfEdges(Graph g) {
var layers = util.buildLayerMatrix(g);
layers.forEach((layer) {
var orderShift = 0;
layer.forEach((v, i) {
Map node = g.node(v);
node["order"] = i + orderShift;
node["selfEdges"].forEach((Map selfEdge) {
util.addDummyNode(g, "selfedge", {
"width": selfEdge["label"].width,
"height": selfEdge["label"].height,
"rank": node["rank"],
"order": i + (++orderShift),
"e": selfEdge["e"],
"label": selfEdge["label"]
}, "_se");
});
node.remove("selfEdges");
});
});
}
positionSelfEdges(Graph g) {
g.nodes.forEach((v) {
Map node = g.node(v);
if (node["dummy"] == "selfedge") {
Map selfNode = g.node(node["e"].v);
var x = selfNode["x"] + selfNode["width"] / 2,
y = selfNode["y"],
dx = node["x"] - x,
dy = selfNode["height"] / 2;
g.setEdge(node["e"], node["label"]);
g.removeNode(v);
node["label"]["points"] = [
{ "x": x + 2 * dx / 3, "y": y - dy },
{ "x": x + 5 * dx / 6, "y": y - dy },
{ "x": x + dx , "y": y },
{ "x": x + 5 * dx / 6, "y": y + dy },
{ "x": x + 2 * dx / 3, "y": y + dy },
];
node["label"]["x"] = node["x"];
node["label"]["y"] = node["y"];
}
});
}
Map selectNumberAttrs(Map obj, Iterable attrs) {
return util.mapValues(util.pick(obj, attrs), (k, v) => num.parse(k));
}
Map canonicalize(Map attrs) {
var newAttrs = {};
attrs.forEach((v, k) {
newAttrs[k.toLowerCase()] = v;
});
return newAttrs;
}