cutter/src/widgets/GraphGridLayout.cpp
2022-08-18 17:18:40 +02:00

1765 lines
76 KiB
C++

#include "GraphGridLayout.h"
#include <unordered_set>
#include <unordered_map>
#include <queue>
#include <stack>
#include <cassert>
#include <queue>
#include "common/BinaryTrees.h"
/** @class GraphGridLayout
Basic familiarity with graph algorithms is recommended.
# Terms used:
- **Vertex**, **node**, **block** - read description of graph for definition. Within this text
vertex and node are used interchangeably with block due to code being written for visualizing basic
block control flow graph.
- **edge** - read description of graph for definition for precise definition.
- **DAG** - directed acyclic graph, graph using directed edges which doesn't have cycles. DAG may
contain loops if following them would require going in both directions of edges. Example 1->2 1->3
3->2 is a DAG, 2->1 1->3 3->2 isn't a DAG.
- **DFS** - depth first search, a graph traversal algorithm
- **toposort** - topological sorting, process of ordering a DAG vertices that all edges go from
vertices earlier in the toposort order to vertices later in toposort order. There are multiple
algorithms for implementing toposort operation. Single DAG can have multiple valid topological
orderings, a toposort algorithm can be designed to prioritize a specific one from all valid toposort
orders. Example: for graph 1->4, 2->1, 2->3, 3->4 valid topological orders are [2,1,3,4] and
[2,3,1,4].
# High level structure of the algorithm
1. select subset of edges that form a DAG (remove cycles)
2. toposort the DAG
3. choose a subset of edges that form a tree and assign layers
4. assign node positions within grid using tree structure, child subtrees are placed side by side
with parent on top
5. perform edge routing
6. calculate column and row pixel positions based on node sizes and amount edges between the rows
7. [optional] layout compacting
Contrary to many other layered graph drawing algorithm this implementation doesn't perform node
reordering to minimize edge crossing. This simplifies implementation, and preserves original control
flow structure for conditional jumps ( true jump on one side, false jump on other). Due to most of
control flow being result of structured programming constructs like if/then/else and loops,
resulting layout is usually readable without node reordering within layers.
# Description of grid.
To simplify the layout algorithm initial steps assume that all nodes have the same size and edges
are zero width. After placing the nodes and routing the edges it is known which nodes are in in
which row and column, how many edges are between each pair of rows. Using this information positions
are converted from the grid cells to pixel coordinates. Routing 0 width edges between rows can also
be interpreted as every second row and column being reserved for edges. The row numbers in code are
using first interpretation. To allow better centering of nodes one above other each node is 2
columns wide and 1 row high.
\image html graph_grid.svg
# 1-2 Cycle removal and toposort
Cycle removal and toposort are done at the same time during single DFS traversal. In case entrypoint
is part of a loop DFS started from entrypoint. This ensures that entrypoint is at the top of
resulting layout if possible. Resulting toposort order is used in many of the following layout steps
that require calculating some property of a vertex based on child property or the other way around.
Using toposort order such operations can be implemented iteration through array in either forward or
reverse direction. To prevent running out of stack memory when processing large graphs DFS is
implemented non-recursively.
# Row assignment
Rows are assigned in toposort order from top to bottom, with nodes row being max(predecessor.row)+1.
This ensures that loop edges are only ones going from deeper levels to previous layers.
To further simply node placement a subset of edges is selected which forms a tree. This turns DAG
drawing problem into a tree drawing problem. For each node in level n following nodes which have
level exactly n+1 are greedily assigned as child nodes in tree. If a node already has parent
assigned then corresponding edge is not part of tree.
# Node position assignment
Since the graph has been reduced to a tree, node placement is more or less putting subtrees side by
side with parent on top. There is some room for interpretation what exactly side by side means and
where exactly on top is. Drawing the graph either too dense or too big may make it less readable so
there are configuration options which allow choosing these things resulting in more or less dense
layout.
Once the subtrees are placed side by side. Parent node can be placed either in the middle of
horizontal bounds or in the middle of direct children. First option results in narrower layout and
more vertical columns. Second option results in nodes being more spread out which may help seeing
where each edge goes.
In more compact mode two subtrees are placed side by side taking into account their shape. In wider
mode bounding box of shorter subtree is used instead of exact shape. This gives slightly sparse
layout without it being too wide.
\image html graph_parent_placement.svg
# Edge routing
Edge routing can be split into: main column selection, rough routing, segment offset calculation.
Transition from source to target row is done using single vertical segment. This is called main
column.
A sweep line is used for computing main columns: Blocks and edges are processed as events top to
bottom based off their row (max(start row, end row) for edges). Blocked columns are tracked in a
tree structure which allows searching nearest column with at least last N rows empty. The column
of the starting block is favored for the main column, otherwise the target block's column is chosen
if it is not blocked. If both the source and target columns are blocked, nearest unblocked column
is chosen. An empty column can always be found, in the worst case there are empty columns at the
sides of drawing. If two columns are equally close, the tie is broken based on whether the edge is a
true or false branch. In case of upward edges it is allowed to choose a column on the outside which
is slightly further than nearest empty to reduce the chance of producing tilted figure 8 shaped
crossing between two blocks.
Rough routing creates the path of edge using up to 5 segments using grid coordinates.
Due to nodes being placed in a grid. Horizontal segments of edges can't intersect with any nodes.
The path for edges is chosen so that it consists of at most 5 segments, typically resulting in
sideways U shape or square Z shape.
- short vertical segment from node to horizontal line
- move to empty column
- vertical segment between starting row and end row
- horizontal segment to target node column
- short vertical segment connecting to target node
There are 3 special cases:
- source and target nodes are in the same column with no nodes between - single vertical segment
- column bellow stating node is empty - segments 1-3 are merged
- column above target node is empty - segments 3-5 are merged
After rough routing segment offsets are calculated relative to their corresponding edge column. This
ensures that two segments don't overlap. Segment offsets within each column are assigned greedily
with some heuristics for assignment order to reduce amount of edge crossings and result in more
visually pleasing output for a typical CFG graph. Each segment gets assigned an offset that is
maximum of previously assigned offsets overlapping with current segment + segment spacing.
Assignment order is chosen based on:
* direction of previous and last segment - helps reducing crossings and place the segments between
nodes
* segment length - reduces crossing when segment endpoints have the same structure as valid
parentheses expression
* edge length - establishes some kind of order when single node is connected to many edges,
typically a block with switch statement or block after switch statement.
# Layout compacting
Doing the layout within a grid causes minimal spacing to be limited by widest and tallest block
within each column and row. One common case is block with function entrypoint being wider due to
function name causing wide horizontal space between branching blocks. Another case is rows in two
parallel columns being aligned.
\image html layout_compacting.svg
Both problems are mitigated by squishing graph. Compressing in each of the two direction is done
separately. The process is defined as liner program. Each variable represents a position of edge
segment or node in the direction being optimized.
Following constraints are used
- Keep the order with nearest segments.
- If the node has two outgoing edges, one to the node on left side and other to the right, keep them
on the corresponding side of node's center.
- For all edges keep the node which is above above. This helps when vertical block spacing is set
bigger than double edge spacing and edge shadows relationship between two blocks.
- Equality constraint to keep relative position between nodes and and segments directly connected to
them.
- Equality constraint to keep the node centered when control flow merges
In the vertical direction objective function minimizes y positions of nodes and lengths of vertical
segments. In the horizontal direction objective function minimizes lengths of horizontal segments.
In the resulting linear program all constraints beside x_i >= 0 consist of exactly two variables:
either x_i - x_j <= c_k or x_i = x_j + c_k.
Since it isn't necessary get perfect solution and to avoid worst case performance current
implementation isn't using a general purpose linear programming solver. Each variable is changed
until constraint is reached and afterwards variables are grouped and changed together.
*/
GraphGridLayout::GraphGridLayout(GraphGridLayout::LayoutType layoutType) : GraphLayout({})
{
switch (layoutType) {
case LayoutType::Narrow:
tightSubtreePlacement = true;
parentBetweenDirectChild = false;
useLayoutOptimization = true;
break;
case LayoutType::Medium:
tightSubtreePlacement = false;
parentBetweenDirectChild = true;
useLayoutOptimization = true;
break;
case LayoutType::Wide:
tightSubtreePlacement = false;
parentBetweenDirectChild = true;
useLayoutOptimization = false;
break;
}
}
std::vector<ut64> GraphGridLayout::topoSort(LayoutState &state, ut64 entry)
{
auto &blocks = *state.blocks;
// Run DFS to:
// * select backwards/loop edges
// * perform toposort
std::vector<ut64> blockOrder;
enum class State : uint8_t { NotVisited = 0, InStack, Visited };
std::unordered_map<ut64, State> visited;
visited.reserve(state.blocks->size());
std::stack<std::pair<ut64, size_t>> stack;
auto dfsFragment = [&visited, &blocks, &state, &stack, &blockOrder](ut64 first) {
visited[first] = State::InStack;
stack.push({ first, 0 });
while (!stack.empty()) {
auto v = stack.top().first;
auto edge_index = stack.top().second;
const auto &block = blocks[v];
if (edge_index < block.edges.size()) {
++stack.top().second;
auto target = block.edges[edge_index].target;
auto &targetState = visited[target];
if (targetState == State::NotVisited) {
targetState = State::InStack;
stack.push({ target, 0 });
state.grid_blocks[v].dag_edge.push_back(target);
} else if (targetState == State::Visited) {
state.grid_blocks[v].dag_edge.push_back(target);
} // else { targetState == 1 in stack, loop edge }
} else {
stack.pop();
visited[v] = State::Visited;
blockOrder.push_back(v);
}
}
};
// Start with entry so that if start of function block is part of loop it
// is still kept at top unless it's impossible to do while maintaining
// topological order.
dfsFragment(entry);
for (auto &blockIt : blocks) {
if (visited[blockIt.first] == State::NotVisited) {
dfsFragment(blockIt.first);
}
}
return blockOrder;
}
void GraphGridLayout::assignRows(GraphGridLayout::LayoutState &state,
const std::vector<unsigned long long> &blockOrder)
{
for (auto it = blockOrder.rbegin(), end = blockOrder.rend(); it != end; it++) {
auto &block = state.grid_blocks[*it];
int nextLevel = block.row + 1;
for (auto target : block.dag_edge) {
auto &targetBlock = state.grid_blocks[target];
targetBlock.row = std::max(targetBlock.row, nextLevel);
}
}
}
void GraphGridLayout::selectTree(GraphGridLayout::LayoutState &state)
{
for (auto &blockIt : state.grid_blocks) {
auto &block = blockIt.second;
for (auto targetId : block.dag_edge) {
auto &targetBlock = state.grid_blocks[targetId];
if (!targetBlock.has_parent && targetBlock.row == block.row + 1) {
block.tree_edge.push_back(targetId);
targetBlock.has_parent = true;
}
}
}
}
void GraphGridLayout::CalculateLayout(GraphLayout::Graph &blocks, ut64 entry, int &width,
int &height) const
{
LayoutState layoutState;
layoutState.blocks = &blocks;
if (blocks.empty()) {
return;
}
if (blocks.find(entry) == blocks.end()) {
entry = blocks.begin()->first;
}
for (auto &it : blocks) {
GridBlock block;
block.id = it.first;
layoutState.grid_blocks[it.first] = block;
}
auto blockOrder = topoSort(layoutState, entry);
computeAllBlockPlacement(blockOrder, layoutState);
for (auto &blockIt : blocks) {
layoutState.edge[blockIt.first].resize(blockIt.second.edges.size());
for (size_t i = 0; i < blockIt.second.edges.size(); i++) {
layoutState.edge[blockIt.first][i].dest = blockIt.second.edges[i].target;
blockIt.second.edges[i].arrow = GraphEdge::Down;
}
}
for (const auto &edgeList : layoutState.edge) {
auto &startBlock = layoutState.grid_blocks[edgeList.first];
startBlock.outputCount = edgeList.second.size();
for (auto &edge : edgeList.second) {
auto &targetBlock = layoutState.grid_blocks[edge.dest];
targetBlock.inputCount++;
}
}
layoutState.columns = 1;
layoutState.rows = 1;
for (auto &node : layoutState.grid_blocks) {
// count is at least index + 1
layoutState.rows = std::max(layoutState.rows, size_t(node.second.row) + 1);
// block is 2 column wide
layoutState.columns = std::max(layoutState.columns, size_t(node.second.col) + 2);
}
layoutState.rowHeight.assign(layoutState.rows, 0);
layoutState.columnWidth.assign(layoutState.columns, 0);
for (auto &node : layoutState.grid_blocks) {
const auto &inputBlock = blocks[node.first];
layoutState.rowHeight[node.second.row] =
std::max(inputBlock.height, layoutState.rowHeight[node.second.row]);
layoutState.columnWidth[node.second.col] =
std::max(inputBlock.width / 2, layoutState.columnWidth[node.second.col]);
layoutState.columnWidth[node.second.col + 1] =
std::max(inputBlock.width / 2, layoutState.columnWidth[node.second.col + 1]);
}
routeEdges(layoutState);
convertToPixelCoordinates(layoutState, width, height);
if (useLayoutOptimization) {
optimizeLayout(layoutState);
cropToContent(blocks, width, height);
}
}
void GraphGridLayout::findMergePoints(GraphGridLayout::LayoutState &state) const
{
for (auto &blockIt : state.grid_blocks) {
auto &block = blockIt.second;
GridBlock *mergeBlock = nullptr;
int grandChildCount = 0;
for (auto edge : block.tree_edge) {
auto &targetBlock = state.grid_blocks[edge];
if (targetBlock.tree_edge.size()) {
mergeBlock = &state.grid_blocks[targetBlock.tree_edge[0]];
}
grandChildCount += targetBlock.tree_edge.size();
}
if (!mergeBlock || grandChildCount != 1) {
continue;
}
int blocksGoingToMerge = 0;
int blockWithTreeEdge = 0;
for (auto edge : block.tree_edge) {
auto &targetBlock = state.grid_blocks[edge];
bool goesToMerge = false;
for (auto secondEdgeTarget : targetBlock.dag_edge) {
if (secondEdgeTarget == mergeBlock->id) {
goesToMerge = true;
break;
}
}
if (goesToMerge) {
if (targetBlock.tree_edge.size() == 1) {
blockWithTreeEdge = blocksGoingToMerge;
}
blocksGoingToMerge++;
} else {
break;
}
}
if (blocksGoingToMerge) {
block.mergeBlock = mergeBlock->id;
state.grid_blocks[block.tree_edge[blockWithTreeEdge]].col =
blockWithTreeEdge * 2 - (blocksGoingToMerge - 1);
}
}
}
void GraphGridLayout::computeAllBlockPlacement(const std::vector<ut64> &blockOrder,
LayoutState &layoutState) const
{
assignRows(layoutState, blockOrder);
selectTree(layoutState);
findMergePoints(layoutState);
// Shapes of subtrees are maintained using linked lists. Each value within list is column
// relative to previous row. This allows moving things around by changing only first value in
// list.
LinkedListPool<int> sides(blockOrder.size() * 2); // *2 = two sides for each node
// Process nodes in the order from bottom to top. Ensures that all subtrees are processed before
// parent node.
for (auto blockId : blockOrder) {
auto &block = layoutState.grid_blocks[blockId];
if (block.tree_edge.size() == 0) {
block.row_count = 1;
block.col = 0;
block.lastRowRight = 2;
block.lastRowLeft = 0;
block.leftPosition = 0;
block.rightPosition = 2;
block.leftSideShape = sides.makeList(0);
block.rightSideShape = sides.makeList(2);
} else {
auto &firstChild = layoutState.grid_blocks[block.tree_edge[0]];
auto leftSide =
firstChild
.leftSideShape; // left side of block children subtrees processed so far
auto rightSide = firstChild.rightSideShape;
block.row_count = firstChild.row_count;
block.lastRowRight = firstChild.lastRowRight;
block.lastRowLeft = firstChild.lastRowLeft;
block.leftPosition = firstChild.leftPosition;
block.rightPosition = firstChild.rightPosition;
// Place children subtrees side by side
for (size_t i = 1; i < block.tree_edge.size(); i++) {
auto &child = layoutState.grid_blocks[block.tree_edge[i]];
int minPos = INT_MIN;
int leftPos = 0;
int rightPos = 0;
auto leftIt = sides.head(rightSide);
auto rightIt = sides.head(child.leftSideShape);
int maxLeftWidth = 0;
int minRightPos = child.col;
while (leftIt
&& rightIt) { // process part of subtrees that touch when put side by side
leftPos += *leftIt;
rightPos += *rightIt;
minPos = std::max(minPos, leftPos - rightPos);
maxLeftWidth = std::max(maxLeftWidth, leftPos);
minRightPos = std::min(minRightPos, rightPos);
++leftIt;
++rightIt;
}
int rightTreeOffset = 0;
if (tightSubtreePlacement) {
rightTreeOffset = minPos; // mode a) place subtrees as close as possible
} else {
// mode b) use bounding box for shortest subtree and full shape of other side
if (leftIt) {
rightTreeOffset = maxLeftWidth - child.leftPosition;
} else {
rightTreeOffset = block.rightPosition - minRightPos;
}
}
// Calculate the new shape after putting the two subtrees side by side
child.col += rightTreeOffset;
if (leftIt) {
*leftIt -= (rightTreeOffset + child.lastRowRight - leftPos);
rightSide =
sides.append(child.rightSideShape, sides.splitTail(rightSide, leftIt));
} else if (rightIt) {
*rightIt += (rightPos + rightTreeOffset - block.lastRowLeft);
leftSide =
sides.append(leftSide, sides.splitTail(child.leftSideShape, rightIt));
rightSide = child.rightSideShape;
block.lastRowRight = child.lastRowRight + rightTreeOffset;
block.lastRowLeft = child.lastRowLeft + rightTreeOffset;
} else {
rightSide = child.rightSideShape;
}
*sides.head(rightSide) += rightTreeOffset;
block.row_count = std::max(block.row_count, child.row_count);
block.leftPosition =
std::min(block.leftPosition, child.leftPosition + rightTreeOffset);
block.rightPosition =
std::max(block.rightPosition, rightTreeOffset + child.rightPosition);
}
int col = 0;
// Calculate parent position
if (parentBetweenDirectChild) {
// mode a) keep one child to the left, other to the right
for (auto target : block.tree_edge) {
col += layoutState.grid_blocks[target].col;
}
col /= block.tree_edge.size();
} else {
// mode b) somewhere between left most direct child and right most, preferably in
// the middle of horizontal dimensions. Results layout looks more like single
// vertical line.
col = (block.rightPosition + block.leftPosition) / 2 - 1;
col = std::max(col, layoutState.grid_blocks[block.tree_edge.front()].col - 1);
col = std::min(col, layoutState.grid_blocks[block.tree_edge.back()].col + 1);
}
block.col += col; // += instead of = to keep offset calculated in previous steps
block.row_count += 1;
block.leftPosition = std::min(block.leftPosition, block.col);
block.rightPosition = std::max(block.rightPosition, block.col + 2);
*sides.head(leftSide) -= block.col;
block.leftSideShape = sides.append(sides.makeList(block.col), leftSide);
*sides.head(rightSide) -= block.col + 2;
block.rightSideShape = sides.append(sides.makeList(block.col + 2), rightSide);
// Keep children positions relative to parent so that moving parent moves whole subtree
for (auto target : block.tree_edge) {
auto &targetBlock = layoutState.grid_blocks[target];
targetBlock.col -= block.col;
}
}
}
// Calculate root positions. Typical function should have one root node that matches with
// entrypoint. There can be more of them in case of switch statement analysis failure,
// unreahable basic blocks or using the algorithm for non control flow graphs.
int nextEmptyColumn = 0;
for (auto &blockIt : layoutState.grid_blocks) {
auto &block = blockIt.second;
if (block.row == 0) { // place all the roots first
auto offset = -block.leftPosition;
block.col += nextEmptyColumn + offset;
nextEmptyColumn = block.rightPosition + offset + nextEmptyColumn;
}
}
// Visit all nodes top to bottom, converting relative positions to absolute.
for (auto it = blockOrder.rbegin(), end = blockOrder.rend(); it != end; it++) {
auto &block = layoutState.grid_blocks[*it];
assert(block.col >= 0);
for (auto childId : block.tree_edge) {
auto &childBlock = layoutState.grid_blocks[childId];
childBlock.col += block.col;
}
}
}
void GraphGridLayout::routeEdges(GraphGridLayout::LayoutState &state) const
{
calculateEdgeMainColumn(state);
roughRouting(state);
elaborateEdgePlacement(state);
}
void GraphGridLayout::calculateEdgeMainColumn(GraphGridLayout::LayoutState &state) const
{
// Find an empty column as close as possible to start or end block's column.
// Use sweep line approach processing events sorted by row top to bottom. Use an appropriate
// tree structure to contain blocks above sweep line and query for nearest column which isn't
// blocked by a block.
struct Event
{
ut64 blockId;
size_t edgeId;
int row;
enum Type { Edge = 0, Block = 1 } type;
};
// create events
std::vector<Event> events;
events.reserve(state.grid_blocks.size() * 2);
for (const auto &it : state.grid_blocks) {
events.push_back({ it.first, 0, it.second.row, Event::Block });
const auto &inputBlock = (*state.blocks)[it.first];
int startRow = it.second.row + 1;
auto gridEdges = state.edge[it.first];
gridEdges.resize(inputBlock.edges.size());
for (size_t i = 0; i < inputBlock.edges.size(); i++) {
auto targetId = inputBlock.edges[i].target;
gridEdges[i].dest = targetId;
const auto &targetGridBlock = state.grid_blocks[targetId];
int endRow = targetGridBlock.row;
events.push_back({ it.first, i, std::max(startRow, endRow), Event::Edge });
}
}
std::sort(events.begin(), events.end(), [](const Event &a, const Event &b) {
if (a.row != b.row) {
return a.row < b.row;
}
return static_cast<int>(a.type) < static_cast<int>(b.type);
});
// process events and choose main column for each edge
PointSetMinTree blockedColumns(state.columns + 1, -1);
for (const auto &event : events) {
if (event.type == Event::Block) {
auto block = state.grid_blocks[event.blockId];
blockedColumns.set(block.col + 1, event.row);
} else {
auto block = state.grid_blocks[event.blockId];
int column = block.col + 1;
auto &edge = state.edge[event.blockId][event.edgeId];
const auto &targetBlock = state.grid_blocks[edge.dest];
auto topRow = std::min(block.row + 1, targetBlock.row);
auto targetColumn = targetBlock.col + 1;
// Prefer using the same column as starting node, it allows reducing amount of segments.
if (blockedColumns.valueAtPoint(column) < topRow) {
edge.mainColumn = column;
} else if (blockedColumns.valueAtPoint(targetColumn)
< topRow) { // next try target block column
edge.mainColumn = targetColumn;
} else {
auto nearestLeft = blockedColumns.rightMostLessThan(column, topRow);
auto nearestRight = blockedColumns.leftMostLessThan(column, topRow);
// There should always be empty column at the sides of drawing
assert(nearestLeft != -1 && nearestRight != -1);
// Choose closest column. Take into account distance to source and target block
// columns.
auto distanceLeft = column - nearestLeft + abs(targetColumn - nearestLeft);
auto distanceRight = nearestRight - column + abs(targetColumn - nearestRight);
// For upward edges try to make a loop instead of 8 shape,
// it is slightly longer but produces less crossing.
if (targetBlock.row < block.row) {
if (targetColumn < column && blockedColumns.valueAtPoint(column + 1) < topRow
&& column - targetColumn <= distanceLeft + 2) {
edge.mainColumn = column + 1;
continue;
} else if (targetColumn > column
&& blockedColumns.valueAtPoint(column - 1) < topRow
&& targetColumn - column <= distanceRight + 2) {
edge.mainColumn = column - 1;
continue;
}
}
if (distanceLeft != distanceRight) {
edge.mainColumn = distanceLeft < distanceRight ? nearestLeft : nearestRight;
} else {
// In case of tie choose based on edge index. Should result in true branches
// being mostly on one side, false branches on other side.
edge.mainColumn = event.edgeId < state.edge[event.blockId].size() / 2
? nearestLeft
: nearestRight;
}
}
}
}
}
void GraphGridLayout::roughRouting(GraphGridLayout::LayoutState &state) const
{
auto getSpacingOverride = [this](int blockWidth, int edgeCount) {
if (edgeCount == 0) {
return 0;
}
int maxSpacing = blockWidth / edgeCount;
if (maxSpacing < layoutConfig.edgeHorizontalSpacing) {
return std::max(maxSpacing, 1);
}
return 0;
};
for (auto &blockIt : state.grid_blocks) {
auto &blockEdges = state.edge[blockIt.first];
for (size_t i = 0; i < blockEdges.size(); i++) {
auto &edge = blockEdges[i];
const auto &start = blockIt.second;
const auto &target = state.grid_blocks[edge.dest];
edge.addPoint(start.row + 1, start.col + 1);
if (edge.mainColumn != start.col + 1) {
edge.addPoint(start.row + 1, start.col + 1,
edge.mainColumn < start.col + 1 ? -1 : 1);
edge.addPoint(start.row + 1, edge.mainColumn, target.row <= start.row ? -2 : 0);
}
int mainColumnKind = 0;
if (edge.mainColumn < start.col + 1 && edge.mainColumn < target.col + 1) {
mainColumnKind = +2;
} else if (edge.mainColumn > start.col + 1 && edge.mainColumn > target.col + 1) {
mainColumnKind = -2;
} else if (edge.mainColumn == start.col + 1 && edge.mainColumn != target.col + 1) {
mainColumnKind = edge.mainColumn < target.col + 1 ? 1 : -1;
} else if (edge.mainColumn == target.col + 1 && edge.mainColumn != start.col + 1) {
mainColumnKind = edge.mainColumn < start.col + 1 ? 1 : -1;
}
edge.addPoint(target.row, edge.mainColumn, mainColumnKind);
if (target.col + 1 != edge.mainColumn) {
edge.addPoint(target.row, target.col + 1, target.row <= start.row ? 2 : 0);
edge.addPoint(target.row, target.col + 1,
target.col + 1 < edge.mainColumn ? 1 : -1);
}
// reduce edge spacing when there is large amount of edges connected to single block
auto startSpacingOverride =
getSpacingOverride((*state.blocks)[start.id].width, start.outputCount);
auto targetSpacingOverride =
getSpacingOverride((*state.blocks)[target.id].width, target.inputCount);
edge.points.front().spacingOverride = startSpacingOverride;
edge.points.back().spacingOverride = targetSpacingOverride;
if (edge.points.size() <= 2) {
if (startSpacingOverride && startSpacingOverride < targetSpacingOverride) {
edge.points.back().spacingOverride = startSpacingOverride;
}
} else {
edge.points[1].spacingOverride = startSpacingOverride;
}
int length = 0;
for (size_t i = 1; i < edge.points.size(); i++) {
length += abs(edge.points[i].row - edge.points[i - 1].row)
+ abs(edge.points[i].col - edge.points[i - 1].col);
}
edge.secondaryPriority = 2 * length + (target.row >= start.row ? 1 : 0);
}
}
}
namespace {
/**
* @brief Single segment of an edge. An edge can be drawn using multiple horizontal and vertical
* segments. x y meaning matches vertical segments. For horizontal segments axis are swapped.
*/
struct EdgeSegment
{
int y0;
int y1;
int x;
int edgeIndex;
int secondaryPriority;
int16_t kind;
int16_t spacingOverride; //< segment spacing override, 0 if default spacing should be used
};
struct NodeSide
{
int x;
int y0;
int y1;
int size; //< block size in the x axis direction
};
}
/**
* @brief Calculate segment offsets relative to their column
*
* Argument naming uses terms for vertical segments, but the function can be used for horizontal
* segments as well.
*
* @param segments Segments that need to be processed.
* @param edgeOffsets Output argument for returning segment offsets relative to their columns.
* @param edgeColumnWidth InOut argument describing how much column with edges take. Initial value
* used as minimal value. May be increased to depending on amount of segments in each column and how
* tightly they are packed.
* @param nodeRightSide Right side of nodes. Used to reduce space reserved for edges by placing them
* between nodes.
* @param nodeLeftSide Same as right side.
* @param columnWidth
* @param H All the segmement and node coordinates y0 and y1 are expected to be in range [0;H)
* @param segmentSpacing The expected spacing between two segments in the same column. Actual
* spacing may be smaller for nodes with many edges.
*/
void calculateSegmentOffsets(std::vector<EdgeSegment> &segments, std::vector<int> &edgeOffsets,
std::vector<int> &edgeColumnWidth,
std::vector<NodeSide> &nodeRightSide,
std::vector<NodeSide> &nodeLeftSide,
const std::vector<int> &columnWidth, size_t H, int segmentSpacing)
{
for (auto &segment : segments) {
if (segment.y0 > segment.y1) {
std::swap(segment.y0, segment.y1);
}
}
std::sort(segments.begin(), segments.end(), [](const EdgeSegment &a, const EdgeSegment &b) {
if (a.x != b.x)
return a.x < b.x;
if (a.kind != b.kind)
return a.kind < b.kind;
auto aSize = a.y1 - a.y0;
auto bSize = b.y1 - b.y0;
if (aSize != bSize) {
if (a.kind != 1) {
return aSize < bSize;
} else {
return aSize > bSize;
}
}
if (a.kind != 1) {
return a.secondaryPriority < b.secondaryPriority;
} else {
return a.secondaryPriority > b.secondaryPriority;
}
return false;
});
auto compareNode = [](const NodeSide &a, const NodeSide &b) { return a.x < b.x; };
sort(nodeRightSide.begin(), nodeRightSide.end(), compareNode);
sort(nodeLeftSide.begin(), nodeLeftSide.end(), compareNode);
RangeAssignMaxTree maxSegment(H, INT_MIN);
auto nextSegmentIt = segments.begin();
auto rightSideIt = nodeRightSide.begin();
auto leftSideIt = nodeLeftSide.begin();
while (nextSegmentIt != segments.end()) {
int x = nextSegmentIt->x;
int leftColumWidth = 0;
if (x > 0) {
leftColumWidth = columnWidth[x - 1];
}
maxSegment.setRange(0, H, -leftColumWidth);
while (rightSideIt != nodeRightSide.end() && rightSideIt->x + 1 < x) {
rightSideIt++;
}
while (rightSideIt != nodeRightSide.end() && rightSideIt->x + 1 == x) {
maxSegment.setRange(rightSideIt->y0, rightSideIt->y1 + 1,
rightSideIt->size - leftColumWidth);
rightSideIt++;
}
while (nextSegmentIt != segments.end() && nextSegmentIt->x == x
&& nextSegmentIt->kind <= 1) {
int y = maxSegment.rangeMaximum(nextSegmentIt->y0, nextSegmentIt->y1 + 1);
if (nextSegmentIt->kind != -2) {
y = std::max(y, 0);
}
y += nextSegmentIt->spacingOverride ? nextSegmentIt->spacingOverride : segmentSpacing;
maxSegment.setRange(nextSegmentIt->y0, nextSegmentIt->y1 + 1, y);
edgeOffsets[nextSegmentIt->edgeIndex] = y;
nextSegmentIt++;
}
auto firstRightSideSegment = nextSegmentIt;
auto middleWidth = std::max(maxSegment.rangeMaximum(0, H), 0);
int rightColumnWidth = 0;
if (x < static_cast<int>(columnWidth.size())) {
rightColumnWidth = columnWidth[x];
}
maxSegment.setRange(0, H, -rightColumnWidth);
while (leftSideIt != nodeLeftSide.end() && leftSideIt->x < x) {
leftSideIt++;
}
while (leftSideIt != nodeLeftSide.end() && leftSideIt->x == x) {
maxSegment.setRange(leftSideIt->y0, leftSideIt->y1 + 1,
leftSideIt->size - rightColumnWidth);
leftSideIt++;
}
while (nextSegmentIt != segments.end() && nextSegmentIt->x == x) {
int y = maxSegment.rangeMaximum(nextSegmentIt->y0, nextSegmentIt->y1 + 1);
y += nextSegmentIt->spacingOverride ? nextSegmentIt->spacingOverride : segmentSpacing;
maxSegment.setRange(nextSegmentIt->y0, nextSegmentIt->y1 + 1, y);
edgeOffsets[nextSegmentIt->edgeIndex] = y;
nextSegmentIt++;
}
auto rightSideMiddle = std::max(maxSegment.rangeMaximum(0, H), 0);
rightSideMiddle =
std::max(rightSideMiddle, edgeColumnWidth[x] - middleWidth - segmentSpacing);
for (auto it = firstRightSideSegment; it != nextSegmentIt; ++it) {
edgeOffsets[it->edgeIndex] =
middleWidth + (rightSideMiddle - edgeOffsets[it->edgeIndex]) + segmentSpacing;
}
edgeColumnWidth[x] = middleWidth + segmentSpacing + rightSideMiddle;
}
}
/**
* @brief Center the segments to the middle of edge columns when possible.
* @param segmentOffsets offsets relative to the left side edge column.
* @param edgeColumnWidth widths of edge columns
* @param segments either all horizontal or all vertical edge segments
*/
static void centerEdges(std::vector<int> &segmentOffsets, const std::vector<int> &edgeColumnWidth,
const std::vector<EdgeSegment> &segments)
{
/* Split segments in each edge column into non intersecting chunks. Center each chunk
* separately.
*
* Process segment endpoints sorted by x and y. Maintain count of currently started segments.
* When number of active segments reaches 0 there is empty space between chunks.
*/
struct Event
{
int x;
int y;
int index;
bool start;
};
std::vector<Event> events;
events.reserve(segments.size() * 2);
for (const auto &segment : segments) {
auto offset = segmentOffsets[segment.edgeIndex];
// Exclude segments which are outside edge column and between the blocks. It's hard to
// ensure that moving them doesn't cause overlap with blocks.
if (offset >= 0 && offset <= edgeColumnWidth[segment.x]) {
events.push_back({ segment.x, segment.y0, segment.edgeIndex, true });
events.push_back({ segment.x, segment.y1, segment.edgeIndex, false });
}
}
std::sort(events.begin(), events.end(), [](const Event &a, const Event &b) {
if (a.x != b.x)
return a.x < b.x;
if (a.y != b.y)
return a.y < b.y;
// Process segment start events before end to ensure that activeSegmentCount doesn't go
// negative and only reaches 0 at the end of chunk.
return int(a.start) > int(b.start);
});
auto it = events.begin();
while (it != events.end()) {
int left, right;
left = right = segmentOffsets[it->index];
auto chunkStart = it++;
int activeSegmentCount = 1;
while (activeSegmentCount > 0) {
activeSegmentCount += it->start ? 1 : -1;
int offset = segmentOffsets[it->index];
left = std::min(left, offset);
right = std::max(right, offset);
it++;
}
int spacing = (edgeColumnWidth[chunkStart->x] - (right - left)) / 2 - left;
for (auto segment = chunkStart; segment != it; segment++) {
if (segment->start) {
segmentOffsets[segment->index] += spacing;
}
}
}
}
/**
* @brief Convert segment coordinates from arbitrary range to continuous range starting at 0.
* @param segments
* @param leftSides
* @param rightSides
* @return Size of compressed coordinate range.
*/
static int compressCoordinates(std::vector<EdgeSegment> &segments, std::vector<NodeSide> &leftSides,
std::vector<NodeSide> &rightSides)
{
std::vector<int> positions;
positions.reserve((segments.size() + leftSides.size()) * 2);
for (const auto &segment : segments) {
positions.push_back(segment.y0);
positions.push_back(segment.y1);
}
for (const auto &segment : leftSides) {
positions.push_back(segment.y0);
positions.push_back(segment.y1);
}
// y0 and y1 in rightSides should match leftSides
std::sort(positions.begin(), positions.end());
auto lastUnique = std::unique(positions.begin(), positions.end());
positions.erase(lastUnique, positions.end());
auto positionToIndex = [&](int position) {
size_t index =
std::lower_bound(positions.begin(), positions.end(), position) - positions.begin();
assert(index < positions.size());
return index;
};
for (auto &segment : segments) {
segment.y0 = positionToIndex(segment.y0);
segment.y1 = positionToIndex(segment.y1);
}
assert(leftSides.size() == rightSides.size());
for (size_t i = 0; i < leftSides.size(); i++) {
leftSides[i].y0 = rightSides[i].y0 = positionToIndex(leftSides[i].y0);
leftSides[i].y1 = rightSides[i].y1 = positionToIndex(leftSides[i].y1);
}
return positions.size();
}
void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state) const
{
int edgeIndex = 0;
auto segmentFromPoint = [&edgeIndex](const Point &point, const GridEdge &edge, int y0, int y1,
int x) {
EdgeSegment segment;
segment.y0 = y0;
segment.y1 = y1;
segment.x = x;
segment.edgeIndex = edgeIndex++;
segment.kind = point.kind;
segment.spacingOverride = point.spacingOverride;
segment.secondaryPriority = edge.secondaryPriority;
return segment;
};
std::vector<EdgeSegment> segments;
std::vector<NodeSide> rightSides;
std::vector<NodeSide> leftSides;
std::vector<int> edgeOffsets;
// Vertical segments
for (auto &edgeListIt : state.edge) {
for (const auto &edge : edgeListIt.second) {
for (size_t j = 1; j < edge.points.size(); j += 2) {
segments.push_back(
segmentFromPoint(edge.points[j], edge,
edge.points[j - 1].row * 2, // edges in even rows
edge.points[j].row * 2, edge.points[j].col));
}
}
}
for (auto &blockIt : state.grid_blocks) {
auto &node = blockIt.second;
auto width = (*state.blocks)[blockIt.first].width;
auto leftWidth = width / 2;
// not the same as leftWidth, you would think that one pixel offset isn't visible, but it is
auto rightWidth = width - leftWidth;
int row = node.row * 2 + 1; // blocks in odd rows
leftSides.push_back({ node.col, row, row, leftWidth });
rightSides.push_back({ node.col + 1, row, row, rightWidth });
}
state.edgeColumnWidth.assign(state.columns + 1, layoutConfig.blockHorizontalSpacing);
state.edgeColumnWidth[0] = state.edgeColumnWidth.back() = layoutConfig.edgeHorizontalSpacing;
edgeOffsets.resize(edgeIndex);
calculateSegmentOffsets(segments, edgeOffsets, state.edgeColumnWidth, rightSides, leftSides,
state.columnWidth, 2 * state.rows + 1,
layoutConfig.edgeHorizontalSpacing);
centerEdges(edgeOffsets, state.edgeColumnWidth, segments);
edgeIndex = 0;
auto copySegmentsToEdges = [&](bool col) {
int edgeIndex = 0;
for (auto &edgeListIt : state.edge) {
for (auto &edge : edgeListIt.second) {
for (size_t j = col ? 1 : 2; j < edge.points.size(); j += 2) {
int offset = edgeOffsets[edgeIndex++];
if (col) {
GraphBlock *block = nullptr;
if (j == 1) {
block = &(*state.blocks)[edgeListIt.first];
} else if (j + 1 == edge.points.size()) {
block = &(*state.blocks)[edge.dest];
}
if (block) {
int blockWidth = block->width;
int edgeColumWidth = state.edgeColumnWidth[edge.points[j].col];
offset = std::max(-blockWidth / 2 + edgeColumWidth / 2, offset);
offset = std::min(edgeColumWidth / 2
+ std::min(blockWidth, edgeColumWidth) / 2,
offset);
}
}
edge.points[j].offset = offset;
}
}
}
};
auto oldColumnWidths = state.columnWidth;
adjustColumnWidths(state);
for (auto &segment : segments) {
auto &offset = edgeOffsets[segment.edgeIndex];
if (segment.kind == -2) {
offset -= (state.edgeColumnWidth[segment.x - 1] / 2 + state.columnWidth[segment.x - 1])
- oldColumnWidths[segment.x - 1];
} else if (segment.kind == 2) {
offset += (state.edgeColumnWidth[segment.x + 1] / 2 + state.columnWidth[segment.x])
- oldColumnWidths[segment.x];
}
}
calculateColumnOffsets(state.columnWidth, state.edgeColumnWidth, state.columnOffset,
state.edgeColumnOffset);
copySegmentsToEdges(true);
// Horizontal segments
// Use exact x coordinates obtained from vertical segment placement.
segments.clear();
leftSides.clear();
rightSides.clear();
edgeIndex = 0;
for (auto &edgeListIt : state.edge) {
for (const auto &edge : edgeListIt.second) {
for (size_t j = 2; j < edge.points.size(); j += 2) {
int y0 = state.edgeColumnOffset[edge.points[j - 1].col] + edge.points[j - 1].offset;
int y1 = state.edgeColumnOffset[edge.points[j + 1].col] + edge.points[j + 1].offset;
segments.push_back(
segmentFromPoint(edge.points[j], edge, y0, y1, edge.points[j].row));
}
}
}
edgeOffsets.resize(edgeIndex);
for (auto &blockIt : state.grid_blocks) {
auto &node = blockIt.second;
auto blockWidth = (*state.blocks)[node.id].width;
int leftSide = state.edgeColumnOffset[node.col + 1]
+ state.edgeColumnWidth[node.col + 1] / 2 - blockWidth / 2;
int rightSide = leftSide + blockWidth;
int h = (*state.blocks)[blockIt.first].height;
int freeSpace = state.rowHeight[node.row] - h;
int topProfile = state.rowHeight[node.row];
int bottomProfile = h;
if (verticalBlockAlignmentMiddle) {
topProfile -= freeSpace / 2;
bottomProfile += freeSpace / 2;
}
leftSides.push_back({ node.row, leftSide, rightSide, topProfile });
rightSides.push_back({ node.row, leftSide, rightSide, bottomProfile });
}
state.edgeRowHeight.assign(state.rows + 1, layoutConfig.blockVerticalSpacing);
state.edgeRowHeight[0] = state.edgeRowHeight.back() = layoutConfig.edgeVerticalSpacing;
edgeOffsets.resize(edgeIndex);
auto compressedCoordinates = compressCoordinates(segments, leftSides, rightSides);
calculateSegmentOffsets(segments, edgeOffsets, state.edgeRowHeight, rightSides, leftSides,
state.rowHeight, compressedCoordinates,
layoutConfig.edgeVerticalSpacing);
copySegmentsToEdges(false);
}
void GraphGridLayout::adjustColumnWidths(GraphGridLayout::LayoutState &state) const
{
state.rowHeight.assign(state.rows, 0);
state.columnWidth.assign(state.columns, 0);
for (auto &node : state.grid_blocks) {
const auto &inputBlock = (*state.blocks)[node.first];
state.rowHeight[node.second.row] =
std::max(inputBlock.height, state.rowHeight[node.second.row]);
int edgeWidth = state.edgeColumnWidth[node.second.col + 1];
int columnWidth = (inputBlock.width - edgeWidth) / 2;
state.columnWidth[node.second.col] =
std::max(columnWidth, state.columnWidth[node.second.col]);
state.columnWidth[node.second.col + 1] =
std::max(columnWidth, state.columnWidth[node.second.col + 1]);
}
}
int GraphGridLayout::calculateColumnOffsets(const std::vector<int> &columnWidth,
std::vector<int> &edgeColumnWidth,
std::vector<int> &columnOffset,
std::vector<int> &edgeColumnOffset)
{
assert(edgeColumnWidth.size() == columnWidth.size() + 1);
int position = 0;
edgeColumnOffset.resize(edgeColumnWidth.size());
columnOffset.resize(columnWidth.size());
for (size_t i = 0; i < columnWidth.size(); i++) {
edgeColumnOffset[i] = position;
position += edgeColumnWidth[i];
columnOffset[i] = position;
position += columnWidth[i];
}
edgeColumnOffset.back() = position;
position += edgeColumnWidth.back();
return position;
}
void GraphGridLayout::convertToPixelCoordinates(GraphGridLayout::LayoutState &state, int &width,
int &height) const
{
// calculate row and column offsets
width = calculateColumnOffsets(state.columnWidth, state.edgeColumnWidth, state.columnOffset,
state.edgeColumnOffset);
height = calculateColumnOffsets(state.rowHeight, state.edgeRowHeight, state.rowOffset,
state.edgeRowOffset);
// block pixel positions
for (auto &block : (*state.blocks)) {
const auto &gridBlock = state.grid_blocks[block.first];
block.second.x = state.edgeColumnOffset[gridBlock.col + 1]
+ state.edgeColumnWidth[gridBlock.col + 1] / 2 - block.second.width / 2;
block.second.y = state.rowOffset[gridBlock.row];
if (verticalBlockAlignmentMiddle) {
block.second.y += (state.rowHeight[gridBlock.row] - block.second.height) / 2;
}
}
// edge pixel positions
for (auto &it : (*state.blocks)) {
auto &block = it.second;
for (size_t i = 0; i < block.edges.size(); i++) {
auto &resultEdge = block.edges[i];
resultEdge.polyline.clear();
resultEdge.polyline.push_back(QPointF(0, block.y + block.height));
const auto &edge = state.edge[it.first][i];
for (size_t j = 1; j < edge.points.size(); j++) {
if (j & 1) { // vertical segment
int column = edge.points[j].col;
int x = state.edgeColumnOffset[column] + edge.points[j].offset;
resultEdge.polyline.back().setX(x);
resultEdge.polyline.push_back(QPointF(x, 0));
} else { // horizontal segment
int row = edge.points[j].row;
int y = state.edgeRowOffset[row] + edge.points[j].offset;
resultEdge.polyline.back().setY(y);
resultEdge.polyline.push_back(QPointF(0, y));
}
}
}
}
connectEdgeEnds(*state.blocks);
}
void GraphGridLayout::cropToContent(GraphLayout::Graph &graph, int &width, int &height) const
{
if (graph.empty()) {
width = std::max(1, layoutConfig.edgeHorizontalSpacing);
height = std::max(1, layoutConfig.edgeVerticalSpacing);
return;
}
const auto &anyBlock = graph.begin()->second;
int minPos[2] = { anyBlock.x, anyBlock.y };
int maxPos[2] = { anyBlock.x, anyBlock.y };
auto updateLimits = [&](int x, int y) {
minPos[0] = std::min(minPos[0], x);
minPos[1] = std::min(minPos[1], y);
maxPos[0] = std::max(maxPos[0], x);
maxPos[1] = std::max(maxPos[1], y);
};
for (const auto &blockIt : graph) {
auto &block = blockIt.second;
updateLimits(block.x, block.y);
updateLimits(block.x + block.width, block.y + block.height);
for (auto &edge : block.edges) {
for (auto &point : edge.polyline) {
updateLimits(point.x(), point.y());
}
}
}
minPos[0] -= layoutConfig.edgeHorizontalSpacing;
minPos[1] -= layoutConfig.edgeVerticalSpacing;
maxPos[0] += layoutConfig.edgeHorizontalSpacing;
maxPos[1] += layoutConfig.edgeVerticalSpacing;
for (auto &blockIt : graph) {
auto &block = blockIt.second;
block.x -= minPos[0];
block.y -= minPos[1];
for (auto &edge : block.edges) {
for (auto &point : edge.polyline) {
point -= QPointF(minPos[0], minPos[1]);
}
}
}
width = maxPos[0] - minPos[0];
height = maxPos[1] - minPos[1];
}
void GraphGridLayout::connectEdgeEnds(GraphLayout::Graph &graph) const
{
for (auto &it : graph) {
auto &block = it.second;
for (size_t i = 0; i < block.edges.size(); i++) {
auto &resultEdge = block.edges[i];
const auto &target = graph[resultEdge.target];
resultEdge.polyline[0].ry() = block.y + block.height;
resultEdge.polyline.back().ry() = target.y;
}
}
}
/// Either equality or inequality x_i <= x_j + c
using Constraint = std::pair<std::pair<int, int>, int>;
/**
* @brief Single pass of linear program optimizer.
* Changes variables until a constraint is hit, afterwards the two variables are changed together.
* @param n number of variables
* @param objectiveFunction coefficients for function \f$\sum c_i x_i\f$ which needs to be minimized
* @param inequalities inequality constraints \f$x_{e_i} - x_{f_i} \leq b_i\f$
* @param equalities equality constraints \f$x_{e_i} - x_{f_i} = b_i\f$
* @param solution input/output argument, returns results, needs to be initialized with a feasible
* solution
* @param stickWhenNotMoving variable grouping strategy
*/
static void optimizeLinearProgramPass(size_t n, std::vector<int> objectiveFunction,
std::vector<Constraint> inequalities,
std::vector<Constraint> equalities,
std::vector<int> &solution, bool stickWhenNotMoving)
{
std::vector<int> group(n);
std::iota(group.begin(), group.end(), 0); // initially each variable is in it's own group
assert(n == objectiveFunction.size());
assert(n == solution.size());
std::vector<size_t> edgeCount(n);
LinkedListPool<size_t> edgePool(inequalities.size() * 2);
std::vector<decltype(edgePool)::List> edges(n);
auto getGroup = [&](int v) {
while (group[v] != v) {
group[v] = group[group[v]];
v = group[v];
}
return v;
};
auto joinGroup = [&](int a, int b) { group[getGroup(b)] = getGroup(a); };
for (auto &constraint : inequalities) {
int a = constraint.first.first;
int b = constraint.first.second;
size_t index = &constraint - &inequalities.front();
edges[a] = edgePool.append(edges[a], edgePool.makeList(index));
edges[b] = edgePool.append(edges[b], edgePool.makeList(index));
edgeCount[a]++;
edgeCount[b]++;
}
std::vector<uint8_t> processed(n);
// Smallest variable value in the group relative to main one, this is used to maintain implicit
// x_i >= 0 constraint
std::vector<int> groupRelativeMin(n, 0);
auto joinSegmentGroups = [&](int a, int b) {
a = getGroup(a);
b = getGroup(b);
joinGroup(a, b);
edgeCount[a] += edgeCount[b];
objectiveFunction[a] += objectiveFunction[b];
int internalEdgeCount = 0;
auto writeIt = edgePool.head(edges[b]);
// update inequalities and remove some of the constraints between variables that are now
// grouped
for (auto it = edgePool.head(edges[b]); it; ++it) {
auto &constraint = inequalities[*it];
int other = constraint.first.first + constraint.first.second - b;
if (getGroup(other)
== a) { // skip inequalities where both variables are now in the same group
internalEdgeCount++;
continue;
}
*writeIt++ = *it;
// Modify the inequalities for the group being attached relative to the main variable in
// the group to which it is being attached.
int diff = solution[a] - solution[b];
if (b == constraint.first.first) {
constraint.first.first = a;
constraint.second += diff;
} else {
constraint.first.second = a;
constraint.second -= diff;
}
}
edges[a] = edgePool.append(edges[a], edgePool.splitHead(edges[b], writeIt));
edgeCount[a] -= internalEdgeCount;
groupRelativeMin[a] =
std::min(groupRelativeMin[a], groupRelativeMin[b] + solution[b] - solution[a]);
};
for (auto &equality : equalities) {
// process equalities, assumes that initial solution is feasible solution and matches
// equality constraints
int a = getGroup(equality.first.first);
int b = getGroup(equality.first.second);
if (a == b) {
equality = { { 0, 0 }, 0 };
continue;
}
// always join smallest group to bigger one
if (edgeCount[a] > edgeCount[b]) {
std::swap(a, b);
// Update the equality equation so that later variable values can be calculated by
// simply iterating through them without need to check which direction the group joining
// was done.
std::swap(equality.first.first, equality.first.second);
equality.second = -equality.second;
}
joinSegmentGroups(b, a);
equality = { { a, b }, solution[a] - solution[b] };
processed[a] = 1;
}
// Priority queue for processing groups starting with currently smallest one. Doing it this way
// should result in number of constraints within group doubling each time two groups are joined.
// That way each constraint is processed no more than log(n) times.
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>,
std::greater<std::pair<int, int>>>
queue;
for (size_t i = 0; i < n; i++) {
if (!processed[i]) {
queue.push({ edgeCount[i], i });
}
}
while (!queue.empty()) {
int g = queue.top().second;
int size = queue.top().first;
queue.pop();
if ((size_t)size != edgeCount[g] || processed[g]) {
continue;
}
int direction = objectiveFunction[g];
if (direction == 0) {
continue;
}
// Find the first constraint which will be hit by changing the variable in the desired
// direction defined by objective function.
int limitingGroup = -1;
int smallestMove = 0;
if (direction < 0) {
smallestMove = INT_MAX;
for (auto it = edgePool.head(edges[g]); it; ++it) {
auto &inequality = inequalities[*it];
if (g == inequality.first.second) {
continue;
}
int other = inequality.first.second;
if (getGroup(other) == g) {
continue;
}
int move = solution[other] + inequality.second - solution[g];
if (move < smallestMove) {
smallestMove = move;
limitingGroup = other;
}
}
} else {
smallestMove = -solution[g] - groupRelativeMin[g]; // keep all variables >= 0
for (auto it = edgePool.head(edges[g]); it; ++it) {
auto &inequality = inequalities[*it];
if (g == inequality.first.first) {
continue;
}
int other = inequality.first.first;
if (getGroup(other) == g) {
continue;
}
int move = solution[other] - inequality.second - solution[g];
if (move > smallestMove) {
smallestMove = move;
limitingGroup = other;
}
}
}
assert(smallestMove != INT_MAX);
if (smallestMove == INT_MAX) {
// Unbound variable, this means that linear program wasn't set up correctly.
// Better don't change it instead of stretching the graph to infinity.
smallestMove = 0;
}
solution[g] += smallestMove;
if (smallestMove == 0 && stickWhenNotMoving == false) {
continue;
}
processed[g] = 1;
if (limitingGroup != -1) {
joinSegmentGroups(limitingGroup, g);
if (!processed[limitingGroup]) {
queue.push({ edgeCount[limitingGroup], limitingGroup });
}
equalities.push_back({ { g, limitingGroup }, solution[g] - solution[limitingGroup] });
} // else do nothing if limited by variable >= 0
}
for (auto it = equalities.rbegin(), end = equalities.rend(); it != end; ++it) {
solution[it->first.first] = solution[it->first.second] + it->second;
}
}
/**
* @brief Linear programming solver
* Does not guarantee optimal solution.
* @param n number of variables
* @param objectiveFunction coefficients for function \f$\sum c_i x_i\f$ which needs to be minimized
* @param inequalities inequality constraints \f$x_{e_i} - x_{f_i} \leq b_i\f$
* @param equalities equality constraints \f$x_{e_i} - x_{f_i} = b_i\f$
* @param solution input/output argument, returns results, needs to be initialized with a feasible
* solution
*/
static void optimizeLinearProgram(size_t n, const std::vector<int> &objectiveFunction,
std::vector<Constraint> inequalities,
const std::vector<Constraint> &equalities,
std::vector<int> &solution)
{
// Remove redundant inequalities
std::sort(inequalities.begin(), inequalities.end());
auto uniqueEnd = std::unique(
inequalities.begin(), inequalities.end(),
[](const Constraint &a, const Constraint &b) { return a.first == b.first; });
inequalities.erase(uniqueEnd, inequalities.end());
static const int ITERATIONS = 1;
for (int i = 0; i < ITERATIONS; i++) {
optimizeLinearProgramPass(n, objectiveFunction, inequalities, equalities, solution, true);
// optimizeLinearProgramPass(n, objectiveFunction, inequalities, equalities, solution,
// false);
}
}
namespace {
struct Segment
{
int x;
int variableId;
int y0, y1;
};
}
static Constraint createInequality(size_t a, int posA, size_t b, int posB, int minSpacing,
const std::vector<int> &positions)
{
minSpacing = std::min(minSpacing, posB - posA);
return { { a, b }, posB - positions[b] - (posA - positions[a]) - minSpacing };
}
/**
* @brief Create inequality constraints from segments which preserves their relative order on single
* axis.
*
* @param segments list of edge segments and block sides
* @param positions initial element positions before optimization
* @param blockCount number of variables representing blocks, it is assumed that segments with
* variableId < \a blockCount represent one side of block.
* @param variableGroup used to check if segments are part of the same edge and spacing can be
* reduced
* @param blockSpacing minimal spacing between blocks
* @param segmentSpacing minimal spacing between two edge segments, spacing may be less if values in
* \a positions are closer than this
* @param inequalities output variable for resulting inequalities, values initially stored in it are
* not removed
*/
static void createInequalitiesFromSegments(std::vector<Segment> segments,
const std::vector<int> &positions,
const std::vector<size_t> &variableGroup, int blockCount,
int blockSpacing, int segmentSpacing,
std::vector<Constraint> &inequalities)
{
// map used as binary search tree y_position -> segment{variableId, x_position}
// It is used to maintain which segment was last seen in the range y_position..
std::map<int, std::pair<int, int>> lastSegments;
lastSegments[-1] = { -1, -1 };
std::sort(segments.begin(), segments.end(),
[](const Segment &a, const Segment &b) { return a.x < b.x; });
for (auto &segment : segments) {
auto startPos = lastSegments.lower_bound(segment.y0);
--startPos; // should never be lastSegment.begin() because map is initialized with segment
// at pos -1
auto lastSegment = startPos->second;
auto it = startPos;
while (it != lastSegments.end() && it->first <= segment.y1) {
int prevSegmentVariable = it->second.first;
int prevSegmentPos = it->second.second;
if (prevSegmentVariable != -1) {
int minSpacing = segmentSpacing;
if (prevSegmentVariable < blockCount && segment.variableId < blockCount) {
// no need to add inequality between two sides of block
if (prevSegmentVariable == segment.variableId) {
++it;
continue;
}
minSpacing = blockSpacing;
} else if (variableGroup[prevSegmentVariable]
== variableGroup[segment.variableId]) {
minSpacing = 0;
}
inequalities.push_back(createInequality(prevSegmentVariable, prevSegmentPos,
segment.variableId, segment.x, minSpacing,
positions));
}
lastSegment = it->second;
++it;
}
if (startPos->first < segment.y0) {
startPos++;
}
lastSegments.erase(startPos, it); // erase segments covered by current one
lastSegments[segment.y0] = {
segment.variableId, segment.x
}; // current segment
// either current segment splitting previous one into two parts or remaining part of
// partially covered segment
lastSegments[segment.y1] = lastSegment;
}
}
void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const
{
std::unordered_map<uint64_t, int> blockMapping;
size_t blockIndex = 0;
for (auto &blockIt : *state.blocks) {
blockMapping[blockIt.first] = blockIndex++;
}
std::vector<size_t> variableGroups(blockMapping.size());
std::iota(variableGroups.begin(), variableGroups.end(), 0);
std::vector<int> objectiveFunction;
std::vector<Constraint> inequalities;
std::vector<Constraint> equalities;
std::vector<int> solution;
auto addObjective = [&](size_t a, int posA, size_t b, int posB) {
objectiveFunction.resize(std::max(objectiveFunction.size(), std::max(a, b) + 1));
if (posA < posB) {
objectiveFunction[b] += 1;
objectiveFunction[a] -= 1;
} else {
objectiveFunction[a] += 1;
objectiveFunction[b] -= 1;
}
};
auto addInequality = [&](size_t a, int posA, size_t b, int posB, int minSpacing) {
inequalities.push_back(createInequality(a, posA, b, posB, minSpacing, solution));
};
auto addBlockSegmentEquality = [&](ut64 blockId, int edgeVariable, int edgeVariablePos) {
int blockPos = (*state.blocks)[blockId].x;
int blockVariable = blockMapping[blockId];
equalities.push_back({ { blockVariable, edgeVariable }, blockPos - edgeVariablePos });
};
auto setFeasibleSolution = [&](size_t variable, int value) {
solution.resize(std::max(solution.size(), variable + 1));
solution[variable] = value;
};
auto copyVariablesToPositions = [&](const std::vector<int> &solution, bool horizontal = false) {
#ifndef NDEBUG
for (auto v : solution) {
assert(v >= 0);
}
#endif
size_t variableIndex = blockMapping.size();
for (auto &blockIt : *state.blocks) {
auto &block = blockIt.second;
for (auto &edge : blockIt.second.edges) {
for (int i = 1 + int(horizontal); i < edge.polyline.size(); i += 2) {
int x = solution[variableIndex++];
if (horizontal) {
edge.polyline[i].ry() = x;
edge.polyline[i - 1].ry() = x;
} else {
edge.polyline[i].rx() = x;
edge.polyline[i - 1].rx() = x;
}
}
}
int blockVariable = blockMapping[blockIt.first];
(horizontal ? block.y : block.x) = solution[blockVariable];
}
};
std::vector<Segment> segments;
segments.reserve(state.blocks->size() * 2 + state.blocks->size() * 2);
size_t variableIndex = state.blocks->size();
size_t edgeIndex = 0;
// horizontal segments
objectiveFunction.assign(blockMapping.size(), 1);
for (auto &blockIt : *state.blocks) {
auto &block = blockIt.second;
int blockVariable = blockMapping[blockIt.first];
for (auto &edge : block.edges) {
auto &targetBlock = (*state.blocks)[edge.target];
if (block.y < targetBlock.y) {
int spacing = block.height + layoutConfig.blockVerticalSpacing;
inequalities.push_back({ { blockVariable, blockMapping[edge.target] }, -spacing });
}
if (edge.polyline.size() < 3) {
continue;
}
for (int i = 2; i < edge.polyline.size(); i += 2) {
int y0 = edge.polyline[i - 1].x();
int y1 = edge.polyline[i].x();
if (y0 > y1) {
std::swap(y0, y1);
}
int x = edge.polyline[i].y();
segments.push_back({ x, int(variableIndex), y0, y1 });
variableGroups.push_back(blockMapping.size() + edgeIndex);
setFeasibleSolution(variableIndex, x);
if (i > 2) {
int prevX = edge.polyline[i - 2].y();
addObjective(variableIndex, x, variableIndex - 1, prevX);
}
variableIndex++;
}
edgeIndex++;
}
segments.push_back({ block.y, blockVariable, block.x, block.x + block.width });
segments.push_back(
{ block.y + block.height, blockVariable, block.x, block.x + block.width });
setFeasibleSolution(blockVariable, block.y);
}
createInequalitiesFromSegments(std::move(segments), solution, variableGroups,
blockMapping.size(), layoutConfig.blockVerticalSpacing,
layoutConfig.edgeVerticalSpacing, inequalities);
objectiveFunction.resize(solution.size());
optimizeLinearProgram(solution.size(), objectiveFunction, inequalities, equalities, solution);
copyVariablesToPositions(solution, true);
connectEdgeEnds(*state.blocks);
// vertical segments
variableGroups.resize(blockMapping.size());
solution.clear();
equalities.clear();
inequalities.clear();
objectiveFunction.clear();
segments.clear();
variableIndex = blockMapping.size();
edgeIndex = 0;
for (auto &blockIt : *state.blocks) {
auto &block = blockIt.second;
for (auto &edge : block.edges) {
if (edge.polyline.size() < 2) {
continue;
}
size_t firstEdgeVariable = variableIndex;
for (int i = 1; i < edge.polyline.size(); i += 2) {
int y0 = edge.polyline[i - 1].y();
int y1 = edge.polyline[i].y();
if (y0 > y1) {
std::swap(y0, y1);
}
int x = edge.polyline[i].x();
segments.push_back({ x, int(variableIndex), y0, y1 });
variableGroups.push_back(blockMapping.size() + edgeIndex);
setFeasibleSolution(variableIndex, x);
if (i > 2) {
int prevX = edge.polyline[i - 2].x();
addObjective(variableIndex, x, variableIndex - 1, prevX);
}
variableIndex++;
}
size_t lastEdgeVariableIndex = variableIndex - 1;
addBlockSegmentEquality(blockIt.first, firstEdgeVariable, edge.polyline[1].x());
addBlockSegmentEquality(edge.target, lastEdgeVariableIndex, segments.back().x);
edgeIndex++;
}
int blockVariable = blockMapping[blockIt.first];
segments.push_back({ block.x, blockVariable, block.y, block.y + block.height });
segments.push_back(
{ block.x + block.width, blockVariable, block.y, block.y + block.height });
setFeasibleSolution(blockVariable, block.x);
}
createInequalitiesFromSegments(std::move(segments), solution, variableGroups,
blockMapping.size(), layoutConfig.blockHorizontalSpacing,
layoutConfig.edgeHorizontalSpacing, inequalities);
objectiveFunction.resize(solution.size());
// horizontal centering constraints
for (auto &blockIt : *state.blocks) {
auto &block = blockIt.second;
int blockVariable = blockMapping[blockIt.first];
if (block.edges.size() == 2) {
auto &blockLeft = (*state.blocks)[block.edges[0].target];
auto &blockRight = (*state.blocks)[block.edges[1].target];
auto middle = block.x + block.width / 2;
if (blockLeft.x + blockLeft.width < middle && blockRight.x > middle) {
addInequality(blockMapping[block.edges[0].target], blockLeft.x + blockLeft.width,
blockVariable, middle, layoutConfig.blockHorizontalSpacing / 2);
addInequality(blockVariable, middle, blockMapping[block.edges[1].target],
blockRight.x, layoutConfig.blockHorizontalSpacing / 2);
auto &gridBlock = state.grid_blocks[blockIt.first];
if (gridBlock.mergeBlock) {
auto &mergeBlock = (*state.blocks)[gridBlock.mergeBlock];
if (mergeBlock.x + mergeBlock.width / 2 == middle) {
equalities.push_back(
{ { blockVariable, blockMapping[gridBlock.mergeBlock] },
block.x - mergeBlock.x });
}
}
}
}
}
optimizeLinearProgram(solution.size(), objectiveFunction, inequalities, equalities, solution);
copyVariablesToPositions(solution);
}