MySql Workbench 布局源码解读

核心思想

通过定义图的能量,并不断迭代获取能量最小的布局。

源码

源码地址:https://github.com/mysql/mysql-workbench/blob/f7a0175471d773ddbac671d9176b13d0c6243637/modules/wb.model/src/wb_model.cpp

MySql Workbench 布局源码解读 

核心步骤:

int Layouter::do_layout() {
    prepare_layout_stages();

    _min_energy = calc_energy();
    int de0_count = 10; // de=0 count
    double de = 1;      // energy delta
    double prev_energy = 0;
    while (de0_count > 0) {
        shuffle();
        de = prev_energy - _min_energy;
        prev_energy = _min_energy;
        if (de == 0)
            --de0_count;
        else
            de0_count = 10;
    }

    // update actual figures with new coords
    for (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. 图数据初始化。
  2. 计算图能量。
  3. 移动节点位置,迭代直到找到能量最小的布局。
  4. 根据计算结果,更新节点位置。

核心步骤

1. 初始化

void Layouter::prepare_layout_stages() {
    double total_w = 0;
    double total_h = 0;
    // 根据节点的连接数进行排序
    std::sort(_figures.begin(), _figures.end(), compare_node_links);
  
    // reset layout
    for (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);
  }
  1. 将节点按照其边的连接数从小到大排序。
  2. 根据节点的面积叠加计算图的面积。
  3. 计算单元格的宽度和高度。(节点的最大高度和宽度)
  4. 节点位置初始化,初始化位置在画布的左上角四分之一处。

MySql Workbench 布局源码解读

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. 遍历位置在可视区之外的节点,能量增加 1,000,000,000,000。
  2. 遍历计算节点两两之间的能量,叠加到图能量中。

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;
          else
            e += _min_dist + overlap_quot * _min_dist / distance;
        } else {
          e += overlap_quot;
        }
      } else {
        e += distance;
        if (is_linked)
          e += distance * distance;
      }
    }
  
    return e;
  }
  1. 按照节点面积排序,节点面积小的是n1,节点面积大的是n2.
  2. 判断两个节点是否存在覆盖问题。如存在,能量增加重叠区域面积的指定倍数。并乘以固定值重叠能量1000

MySql Workbench 布局源码解读 

e += \_min\_dist * 1 / (distance) * 100 + (x_{12}-x_{21})*(y_{22}-y_{11})

  1. 计算两个节点之间的距离 distance_to_node。
  2. 判断两个节点之间的距离是否小于指定的最小间距。如果小于,增加固定值能量除以distance。

MySql Workbench 布局源码解读 

e += \_min\_dist + overlap\_quot * 1 / distance

MySql Workbench 布局源码解读 

e += \_min\_dist + overlap\_quot * \_min\_dist / distance

  1. 如果两个节点之间有边链接,能量值叠加节点间距离的平方。

MySql Workbench 布局源码解读

e += distance * distance

  1. 其他情况,能量值叠加节点间距。

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);
      else
        l1 = 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);
      else
        l1 = l2 = dx ? ::fabs(dx / sin(qr)) : ::fabs(dy);
    } else {
      dy = y21 - y12;
      if (abs(dcx) > (x12 - x11) / 2)
        dx = x11 - x22;
      else
        dx = dcx;
      if (dy > dx)
        l1 = l2 = dy ? ::fabs(dy / cos(qr)) : ::fabs(dx);
      else
        l1 = 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;
  }

在解读代码之前,先来回顾一下三角函数的计算。

MySql Workbench 布局源码解读 

c++中 atan2 求方位角。 其计算公式如下,其中arctan(a/b) = A  ,arctan是tan的反函数。

MySql Workbench 布局源码解读 

MySql Workbench 布局源码解读                      MySql Workbench 布局源码解读

然后我们再回归到源码实现部分。

情况1:角度>90。

l1 = fabs(dy / cos(qr))

又因为    atan2(dx,dy) = arctan(dy/dx) = qr

=>  tan(qr) = dy/dx

 

=>  cos(qr) = dx/\sqrt{dx^2+dy^2}

=>   l1 = fabs(dy / dx * \sqrt{dx^2+dy^2})

MySql Workbench 布局源码解读

和直接用欧拉距离对比,增加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;
        } else          
          n.move_by(-wsteps[ns], -hsteps[ns]);  // 回归原位
      }
    }
    
    // 重新计算图整体的能量
    if (found_smaller_energy)
      _min_energy = calc_energy();
  
	return found_smaller_energy;
}
  1. 生成随机移动步数step。
  2. 遍历节点。计算节点的能量值
  3. 分别将节点从上下左右四个方向,分别移动wstep和hstep位置。
  4. 然后重新计算节点的能量。
  5. 如果节点能量有变小,则标记found_smaller_energy 为true,表示找到了更合适的布局。否则,节点位置进行回退。
  6. 返回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;
}

思考

和之前看的正交布局相关论文相比,这个算法的实现还是比较简单的。问题:

  1. 在判断是否最优值的时候,仅仅判断了此次计算结果和上次计算结果是否一致,容易导致产生局部最优解。
  2. 一次迭代的时间复杂度过高,n*n*n。
  3. 不知道这个能量计算公式的来由,是否最佳?
  4. 节点重叠的问题,只在能量计算中考虑,并不能完全避免结果中没有重叠的问题。
  5. 节点移动距离,单元格是按照最大的节点来定的。这样会不会产生稀疏图。 

上一篇:阿里云首度公布策略无意布局云基础设施


下一篇:开源机器人 UBR-1 面世