blob: c5915d5460783f34c24525ec165d120355418885 [file] [log] [blame]
library graphlib.layout.util;
import "dart:math" as Math;
import "dart:collection" show SplayTreeMap;
import "../graph.dart" show Graph;
import "lodash.dart";
export "lodash.dart";
//module.exports = {
// addDummyNode: addDummyNode,
// simplify: simplify,
// asNonCompoundGraph: asNonCompoundGraph,
// successorWeights: successorWeights,
// predecessorWeights: predecessorWeights,
// intersectRect: intersectRect,
// buildLayerMatrix: buildLayerMatrix,
// normalizeRanks: normalizeRanks,
// removeEmptyRanks: removeEmptyRanks,
// addBorderNode: addBorderNode,
// maxRank: maxRank,
// partition: partition,
// time: time,
// notime: notime
//};
/// Adds a dummy node to the graph and return v.
addDummyNode(Graph g, type, Map attrs, name) {
var v;
do {
v = uniqueId(name);
} while (g.hasNode(v));
attrs["dummy"] = type;
g.setNode(v, attrs);
return v;
}
/// Returns a new graph with only simple edges. Handles aggregation of data
/// associated with multi-edges.
Graph simplify(Graph g) {
var simplified = new Graph()..setGraph(g.graph());
g.nodes.forEach((v) { simplified.setNode(v, g.node(v)); });
g.edges.forEach((e) {
Map simpleLabel = simplified.edge(e.v, e.w),
label = g.edgeObj(e);
if (simpleLabel == null) {
simpleLabel = { "weight": 0, "minlen": 1 };
}
simplified.setEdge(e.v, e.w, {
"weight": simpleLabel["weight"] + label["weight"],
"minlen": Math.max(simpleLabel["minlen"], label["minlen"])
});
});
return simplified;
}
Graph asNonCompoundGraph(Graph g) {
var simplified = new Graph(multigraph: g.isMultigraph)..setGraph(g.graph());
g.nodes.forEach((v) {
if (g.children(v).length == 0) {
simplified.setNode(v, g.node(v));
}
});
g.edges.forEach((e) {
simplified.setEdge(e, g.edgeObj(e));
});
return simplified;
}
Map successorWeights(Graph g) {
var weightMap = g.nodes.map((v) {
var sucs = {};
g.outEdges(v).forEach((e) {
if (!sucs.containsKey(e.w)) sucs[e.w] = 0;
sucs[e.w] = sucs[e.w] + g.edgeObj(e)["weight"];
});
return sucs;
});
return new Map.fromIterables(g.nodes, weightMap);
}
Map predecessorWeights(Graph g) {
var weightMap = g.nodes.map((v) {
var preds = {};
g.inEdges(v).forEach((e) {
if (!preds.containsKey(e.v)) preds[e.v] = 0;
preds[e.v] = preds[e.v] + g.edgeObj(e).weight;
});
return preds;
});
return new Map.fromIterables(g.nodes, weightMap);
}
/// Finds where a line starting at point ({x, y}) would intersect a rectangle
/// ({x, y, width, height}) if it were pointing at the rectangle's center.
Map intersectRect(Map rect, Map point) {
var x = rect["x"];
var y = rect["y"];
// Rectangle intersection algorithm from:
// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
var dx = point["x"] - x;
var dy = point["y"] - y;
var w = rect["width"] / 2;
var h = rect["height"] / 2;
if (dx == 0 && dy == 0) {
throw new LayoutException("Not possible to find intersection inside of the rectangle");
}
var sx, sy;
if (dy.abs() * w > dx.abs() * h) {
// Intersection is top or bottom of rect.
if (dy < 0) {
h = -h;
}
sx = h * dx / dy;
sy = h;
} else {
// Intersection is left or right of rect.
if (dx < 0) {
w = -w;
}
sx = w;
sy = w * dy / dx;
}
return { "x": x + sx, "y": y + sy };
}
/// Given a DAG with each node assigned "rank" and "order" properties, this
/// function will produce a matrix with the ids of each node.
List buildLayerMatrix(Graph g) {
var layering = range(maxRank(g) + 1).map((_) => new SplayTreeMap()).toList();
g.nodes.forEach((v) {
Map node = g.node(v);
var rank = node["rank"];
if (rank != null) {
layering[rank][node["order"]] = v;
}
});
return layering.map((SplayTreeMap m) => m.values.toList()).toList();
}
/// Adjusts the ranks for all nodes in the graph such that all nodes v have
/// rank(v) >= 0 and at least one node w has rank(w) = 0.
normalizeRanks(Graph g) {
var minimum = min(g.nodes.map((v) => g.node(v)["rank"]));
g.nodes.forEach((v) {
Map node = g.node(v);
if (node.containsKey("rank")) {
node["rank"] -= minimum;
}
});
}
removeEmptyRanks(Graph g) {
// Ranks may not start at 0, so we need to offset them
var offset = min(g.nodes.map((v) => g.node(v).rank));
var layers = new SplayTreeMap();
g.nodes.forEach((v) {
var rank = g.node(v)["rank"] - offset;
if (!layers.containsKey(rank)) {
layers[rank] = [];
}
layers[rank].add(v);
});
var delta = 0,
nodeRankFactor = g.graph()["nodeRankFactor"],
i = 0;
layers.values.forEach((vs) {
if (vs == null && i % nodeRankFactor != 0) {
--delta;
} else if (delta != 0) {
vs.forEach((v) { g.node(v)["rank"] += delta; });
}
i++;
});
}
addBorderNode(g, prefix, [rank, order]) {
var node = {
"width": 0,
"height": 0
};
if (rank != null) {
node["rank"] = rank;
}
if (order != null) {
node["order"] = order;
}
return addDummyNode(g, "border", node, prefix);
}
maxRank(g) {
return max(g.nodes.map((v) {
var rank = g.node(v)["rank"];
if (rank != null) {
return rank;
}
}));
}
/// Partition a collection into two groups: `lhs` and `rhs`. If the supplied
/// function returns true for an entry it goes into `lhs`. Otherwise it goes
/// into `rhs.
Map partition(Iterable collection, Function fn) {
var result = { "lhs": [], "rhs": [] };
collection.forEach((value) {
if (fn(value)) {
result["lhs"].add(value);
} else {
result["rhs"].add(value);
}
});
return result;
}
/// Returns a new function that wraps `fn` with a timer. The wrapper logs the
/// time it takes to execute the function.
time(String name, Function fn, [log(String s) = print]) {
var start = new DateTime.now().millisecondsSinceEpoch;
try {
return fn();
} finally {
log("$name time: ${new DateTime.now().millisecondsSinceEpoch - start}ms");
}
}
notime(String name, Function fn) {
return fn();
}
class LayoutException implements Exception {
final String message;
LayoutException(this.message);
String toString() => message;
}