核心思想
源码
int Layouter::do_layout() {prepare_layout_stages();_min_energy = calc_energy();int de0_count = 10; // de=0 countdouble de = 1; // energy deltadouble prev_energy = 0;while (de0_count > 0) {shuffle();de = prev_energy - _min_energy;prev_energy = _min_energy;if (de == 0)--de0_count;elsede0_count = 10;}// update actual figures with new coordsfor (std::size_t i = 0; i < _figures.size(); ++i) {Node &n = _figures[i];model_FigureRef &f = n.fig;f->left(n.x1);f->top(n.y1);}}
- 图数据初始化。
- 计算图能量。
- 移动节点位置,迭代直到找到能量最小的布局。
-
核心步骤
1. 初始化
void Layouter::prepare_layout_stages() {double total_w = 0;double total_h = 0;// 根据节点的连接数进行排序std::sort(_figures.begin(), _figures.end(), compare_node_links);// reset layoutfor (size_t i = 0; i < _figures.size(); ++i) {Node &n = _figures[i];// place all tables in some initial position// 节点初始位置n.move((long)_w / 4, (long)_h / 4);// Calculate total dimensions and find max cell size.// 计算图最小的面积。cell为节点最大的宽度和高度total_w += n.w;total_h += n.h;if (_cell_w < n.w)_cell_w = (int)n.w;if (_cell_h < n.h)_cell_h = (int)n.h;}// 单元格放大1.1倍_cell_w = (int)(1.1 * _cell_w);}
将节点按照其边的连接数从小到大排序。
- 根据节点的面积叠加计算图的面积。
- 计算单元格的宽度和高度。(节点的最大高度和宽度)
- 节点位置初始化,初始化位置在画布的左上角四分之一处。
2. 计算图能量
double Layouter::calc_energy() {double e = 0.0;std::size_t size = _figures.size();for (std::size_t i = 0; i < size; ++i) {const Node &node = _figures[i];// 位置在可视区之外的节点,能量加很大的一个值if ((node.x1 < 0) || (node.y1 < 0) || (node.x2 + 20 > _w) || (node.y2 + 20 > _h))e += 1000000000000.0;// 计算任意两个节点之间的能量for (std::size_t j = i + 1; j < size; ++j) {if (j >= size)break;e += calc_node_pair(i, j);}}return e;}
计算图整体的能量。
- 遍历位置在可视区之外的节点,能量增加 1,000,000,000,000。
-
3. 计算节点之间能量
double Layouter::calc_node_pair(const std::size_t i1, const std::size_t i2) {const Node *n1 = &(_figures[i1]);const Node *n2 = &(_figures[i2]);const bool is_linked = n1->is_linked_to(i2) || n2->is_linked_to(i1);// 计算节点的面积,节点面积从小到大long S1 = n1->w * n1->h;long S2 = n2->w * n2->h;if (S1 > S2) {std::swap(n1, n2);std::swap(S1, S2);}const long x11 = n1->x1;const long y11 = n1->y1;const long x12 = n1->x2;const long y12 = n1->y2;const long x21 = n2->x1;const long y21 = n2->y1;const long x22 = n2->x2;const long y22 = n2->y2;const long cx1 = x11 + (x12 - x11) / 2;const long cy1 = y11 + (y12 - y11) / 2;const long cx2 = x21 + (x22 - x21) / 2;const long cy2 = y21 + (y22 - y21) / 2;// Detect if nodes overlap 检查节点之间是否存在覆盖问题const bool is_overlap = ((x12 >= x21) && (x22 >= x11) && (y12 >= y21) && (y22 >= y11));static const double overlap_quot = 1000.0;double e = 0.0;double distance = 0;if (is_overlap) {distance = line_len2(cx1, cy1, cx2, cy2);// calc area of overlap 计算重复区域的坐标和面积const long sx1 = x11 > x21 ? x11 : x21;const long sy1 = y11 > y21 ? y11 : y21;const long sx2 = x12 < x22 ? x12 : x22;const long sy2 = y12 < y22 ? y12 : y22;const long dsx = sx2 - sx1;const long dsy = sy2 - sy1;const long Sov = dsx * dsy;if (distance == 0.0)distance = 0.0000001;e = _min_dist * 1 / distance * 100 + Sov;e *= overlap_quot;} else {bool is_horiz = false;distance = distance_to_node(i1, i2, &is_horiz);if (distance <= _min_dist) {if (distance != 0) {if (is_linked)e += _min_dist + overlap_quot * 1 / distance;elsee += _min_dist + overlap_quot * _min_dist / distance;} else {e += overlap_quot;}} else {e += distance;if (is_linked)e += distance * distance;}}return e;}
按照节点面积排序,节点面积小的是n1,节点面积大的是n2.
- 判断两个节点是否存在覆盖问题。如存在,能量增加重叠区域面积的指定倍数。并乘以固定值重叠能量1000
- 计算两个节点之间的距离 distance_to_node。
- 判断两个节点之间的距离是否小于指定的最小间距。如果小于,增加固定值能量除以distance。
- 如果两个节点之间有边链接,能量值叠加节点间距离的平方。

-
4. 节点距离计算
long Layouter::distance_to_node(const std::size_t i1, const std::size_t i2, bool *is_horiz) {const Node &n1 = _figures[i1];const Node &n2 = _figures[i2];const long x11 = n1.x1;const long y11 = n1.y1;const long x12 = n1.x2;const long y12 = n1.y2;const long x21 = n2.x1;const long y21 = n2.y1;const long x22 = n2.x2;const long y22 = n2.y2;const long cx1 = x11 + (x12 - x11) / 2;const long cy1 = y11 + (y12 - y11) / 2;const long cx2 = x21 + (x22 - x21) / 2;const long cy2 = y21 + (y22 - y21) / 2;const long dcx = cx2 - cx1;// 两个节点间的方位角const double qr = atan2((double)dcx, (double)(cy2 - cy1));double dx = 0;double dy = 0;double l1 = 0;double l2 = 0;// 如果大于90度。l1 l2 为欧式距离if (qr > M_PI_2) {dy = y11 - y22;dx = x21 - x12;l1 = dy ? ::fabs(dy / cos(qr)) : ::fabs(dx);l2 = dx ? ::fabs(dx / sin(qr)) : ::fabs(dy);} else if (0.0 < qr && qr <= M_PI_2) {dy = y21 - y12;dx = x21 - x12;if (dy > dx)l1 = l2 = dy ? ::fabs(dy / cos(qr)) : ::fabs(dx);elsel1 = l2 = dx ? ::fabs(dx / sin(qr)) : ::fabs(dy);} else if (qr < -M_PI_2) {dy = y11 - y22;dx = -(x22 - x11);if (dy > dx)l1 = l2 = dy ? ::fabs(dy / cos(qr)) : ::fabs(dx);elsel1 = l2 = dx ? ::fabs(dx / sin(qr)) : ::fabs(dy);} else {dy = y21 - y12;if (abs(dcx) > (x12 - x11) / 2)dx = x11 - x22;elsedx = dcx;if (dy > dx)l1 = l2 = dy ? ::fabs(dy / cos(qr)) : ::fabs(dx);elsel1 = l2 = (dx && qr != 0.0) ? ::fabs(dx / sin(qr)) : ::fabs(dy);}// printf("qr %f (cos(qr) = %f, sin(rq) = %f), l1 %li, l2 %li, dy %li, dx %li\n", qr, cos(qr), sin(qr), l1, l2, dy,// dx);const double aqr = ::fabs(qr);// 判断是否水平,角度if (is_horiz)*is_horiz = PI_38 < aqr && aqr < PI_58;return l1 < l2 ? (long)l1 : (long)l2;}
在解读代码之前,先来回顾一下三角函数的计算。
c++中 atan2 求方位角。 其计算公式如下,其中arctan(a/b) = A ,arctan是tan的反函数。

然后我们再回归到源码实现部分。
情况1:角度>90。
, 又因为
=>
=>
=> 
和直接用欧拉距离对比,增加dy/dx的斜率比,可以更好的考虑整个图的长宽比。
5. 移动节点
移动节点找到可以使图能量最小的布局。
bool Layouter::shuffle() {bool found_smaller_energy = false;// 0-5的随机数const int step = (rand() % 5) + 1;for (std::size_t i = 0; i < _figures.size(); ++i) {Node &n = _figures[i];const int wstep = _cell_w * step;const int hstep = _cell_w * step;double node_energy = calc_node_energy(i, n);const int wsteps[] = {wstep, -wstep, 0, 0};const int hsteps[] = {0, 0, hstep, -hstep};for (int ns = sizeof(wsteps) / sizeof(int) - 1; ns >= 0; --ns) {// 节点朝上下左右四个方向移动,找到能量最小的那个位置n.move_by(wsteps[ns], hsteps[ns]);// 计算移动后节点的能量const double energy = calc_node_energy(i, n);if (energy < node_energy) {node_energy = energy;found_smaller_energy = true;} elsen.move_by(-wsteps[ns], -hsteps[ns]); // 回归原位}}// 重新计算图整体的能量if (found_smaller_energy)_min_energy = calc_energy();return found_smaller_energy;}
- 生成随机移动步数step。
- 遍历节点。计算节点的能量值
- 分别将节点从上下左右四个方向,分别移动wstep和hstep位置。
- 然后重新计算节点的能量。
- 如果节点能量有变小,则标记found_smaller_energy 为true,表示找到了更合适的布局。否则,节点位置进行回退。
- 返回found_smaller_energy。
节点能量计算方法:和剩余的节点的能量之和。
double Layouter::calc_node_energy(const std::size_t node_i, const Node &node) {double e = 0.0;if ((node.x1 < 0) || (node.y1 < 0) || (node.x2 + 20 > _w) || (node.y2 + 20 > _h))e += 1000000000000.0;for (std::size_t i = 0; i < _figures.size(); ++i) {if (node_i != i)e += calc_node_pair(node_i, i);}return e;}
思考
和之前看的正交布局相关论文相比,这个算法的实现还是比较简单的。存在问题:
- 在判断是否最优值的时候,仅仅判断了此次计算结果和上次计算结果是否一致,容易导致产生局部最优解。
- 一次迭代的时间复杂度过高,nnn。
- 节点重叠的问题,只在能量计算中考虑,所以并不能完全避免结果中没有重叠的问题。
- 节点移动距离,单元格是按照最大的节点来定的。这样会产生稀疏图。
