MySql Workbench 布局源码解读

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 核心思想通过定义图的能量,并不断迭代获取能量最小的布局。源码源码地址:https://github.com/mysql/mysql-workbench/blob/f7a0175471d773ddbac671d9176b13d0c6243637/modules/wb.model/src/wb_model.cpp 核心步骤:int Layouter::do_layout() {    prepare

核心思想

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

源码

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

 

核心步骤:

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. 节点位置初始化,初始化位置在画布的左上角四分之一处。

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

 

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

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

 

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

 

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

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

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;
  }

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

 

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

 

                      

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

情况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})

和直接用欧拉距离对比,增加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. 节点移动距离,单元格是按照最大的节点来定的。这样会不会产生稀疏图。 

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
18天前
|
监控 数据可视化 关系型数据库
微服务架构+Java+Spring Cloud +UniApp +MySql智慧工地系统源码
项目管理:项目名称、施工单位名称、项目地址、项目地址、总造价、总面积、施工准可证、开工日期、计划竣工日期、项目状态等。
319 6
|
18天前
|
JSON 关系型数据库 数据库
【python】Python将100个PDF文件对应的json文件存储到MySql数据库(源码)【独一无二】
【python】Python将100个PDF文件对应的json文件存储到MySql数据库(源码)【独一无二】
【python】Python将100个PDF文件对应的json文件存储到MySql数据库(源码)【独一无二】
|
18天前
|
JSON 关系型数据库 数据库
【python】Python将100个PDF文件对应的json文件存储到MySql数据库(源码)【独一无二】
【python】Python将100个PDF文件对应的json文件存储到MySql数据库(源码)【独一无二】
|
18天前
|
运维 监控 安全
云HIS医疗管理系统源码——技术栈【SpringBoot+Angular+MySQL+MyBatis】
云HIS系统采用主流成熟技术,软件结构简洁、代码规范易阅读,SaaS应用,全浏览器访问前后端分离,多服务协同,服务可拆分,功能易扩展;支持多样化灵活配置,提取大量公共参数,无需修改代码即可满足不同客户需求;服务组织合理,功能高内聚,服务间通信简练。
45 4
|
13天前
|
监控 NoSQL Java
java云MES 系统源码Java+ springboot+ mysql 一款基于云计算技术的企业级生产管理系统
MES系统是生产企业对制造执行系统实施的重点在智能制造执行管理领域,而MES系统特点中的可伸缩、信息精确、开放、承接、安全等也传递出:MES在此管理领域中无可替代的“王者之尊”。MES制造执行系统特点集可伸缩性、精确性、开放性、承接性、经济性与安全性于一体,帮助企业解决生产中遇到的实际问题,降低运营成本,快速适应企业不断的制造执行管理需求,使得企业已有基础设施与一切可用资源实现高度集成,提升企业投资的有效性。
60 5
|
18天前
|
Java 数据挖掘 BI
Java医院绩效考核系统源码B/S+avue+MySQL助力医院实现精细化管理
医院绩效考核系统目标是实现对科室、病区财务指标、客户指标、流程指标、成长指标的全面考核、分析,并与奖金分配、学科建设水平评价挂钩。
44 0
|
18天前
|
传感器 人工智能 前端开发
JAVA语言VUE2+Spring boot+MySQL开发的智慧校园系统源码(电子班牌可人脸识别)Saas 模式
智慧校园电子班牌,坐落于班级的门口,适合于各类型学校的场景应用,班级学校日常内容更新可由班级自行管理,也可由学校统一管理。让我们一起看看,电子班牌有哪些功能呢?
113 4
JAVA语言VUE2+Spring boot+MySQL开发的智慧校园系统源码(电子班牌可人脸识别)Saas 模式
|
18天前
|
前端开发 IDE Java
基于Springboot+MYSQL+Maven实现的宠物医院管理系统(源码+数据库+运行指导文档+项目运行指导视频)
基于Springboot+MYSQL+Maven实现的宠物医院管理系统(源码+数据库+运行指导文档+项目运行指导视频)
187 0
|
18天前
|
Java 关系型数据库 MySQL
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的应用。
38 0
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
|
18天前
|
缓存 小程序
Java+saas模式 智慧校园系统源码MySQL5.7+ elmentui前后端分离架构 让校园管理更高效的数字化平台系统源码
智慧校园是在数字通增强版基础上,研发的一套面向教育行业的数字化校园软件,其显著特点是集学校网站、协同办公、即时通讯、网络空间、移动办公于一体。在满足教职工日常办公需要的同时,拥有诸多教育行业功能,并提供便捷易用的“家校通”平台以满足老师、学生、家长的日常交流。数字通智慧校园教育版中的协同办公、即时通讯、移动办公等功能模块随通用版一同改进,将网络办公最新技术应用到教育行业。
49 1