你可能不知道的前端算法之文字避让(inMap)


Posted in Javascript onJanuary 12, 2018

前言

inMap 是一款基于 canvas 的大数据可视化库,专注于大数据方向点线面的可视化效果展示。目前支持散点、围栏、热力、网格、聚合等方式;致力于让大数据可视化变得简单易用。

GitHub 地址:https://github.com/TalkingData/inmap(本地下载)

文档地址:http://inmap.talkingdata.com/

在地理信息可视化中,我们经常会遇到在地图上标记文字的需求,下面展示的是某流行 chart 图表框架的效果:

你可能不知道的前端算法之文字避让(inMap)

要显示的文字空间不够时,就会造成文字重叠显示混乱,用户体验很不友好。

怎么解决这个问题呢?我们采用文字避让算法,解决这种坑爹的问题。

下面展示的是 inMap 文字避让效果:

你可能不知道的前端算法之文字避让(inMap)

文字标注算法是 GIS 中最复杂的问题之一(属于 NP 复杂度问题,所以通常不能找到最优解,只能找到较优解)。

inMap 避让算法采用的是四分位模型算法,接下来手把手教你写避让算法,老司机带你装逼带你飞。

准备数据

inMap 接收的是经纬度数据,需要把它映射到 canvas 的像素坐标,这就用到了墨卡托转换,墨卡托算法很复杂,以后我们会有单独的一篇文章来讲讲他的原理。经过转换,你得到的数据应该是这样的:

[
 {
 "name": "海门",//要显示的文字
 "lng": 121.15,
 "lat": 31.89,
 "count": 7,
 "pixel": { //像素坐标
  "x": 968,
  "y": 736
 }
 },
 {
 "name": "鄂尔多斯",
 "lng": 109.781327,
 "lat": 39.608266,
 "count": 5,
 "pixel": {
  "x": 659,
  "y": 478
 }
 },
...
]

好了,我们得到转换后的像素坐标数据(x、y),就可以做下面的事情了。

求出每段文字矩形的实际大小

measureText() 是 canvas 内置的方法,返回字体宽度的像素单位:

let ctx = this.container.getContext('2d'); // canvas 上下文
let width= ctx.measureText(name).width;

我们通过 measureText 得到每个文字的宽度,canvas 并没有直接获取文字的方法,那文字的高度如何的得到呢?

我们通过反复测试发现 canvas 的 font 等于 “13px Arial” 字体(别的字体不敢保证)的时候,文字的高度大概是 fontSize 的 1.1 倍。

所以代码如下:

let fontSize = parseInt(ctx.font);
let height = fontSize * 1.1;

文字的宽度和高度得到后,我们就可以创建文字矩形的坐标系了。

创建四分位模型

你可能不知道的前端算法之文字避让(inMap)

所谓四分位模型,每一个标记点都有上下左右四个放文字的位子,如果左边放不下,那就放右边试试,还不行就放到下面试试,以此类推,原理就这么简单,哈哈。

创建右侧虚拟矩形坐标描述:

你可能不知道的前端算法之文字避让(inMap)

右侧虚拟矩形坐标的描述把圆点也包含在内了,是为了防止文字和圆点重叠。

在计算虚拟矩形的高度时有些坑,圆点大小不是固定的,是根据用户动态配置的,圆点的直径可能大于文字的高度,我们就设定虚拟矩形的高度永远都是最大的那个,需要做一些特殊处理。

代码如下:

_getLeftAnchor() {
  let x = this.center.x - this.radius - this.textReact.width,
    y = this.center.y - this.textReact.height / 2,
    diam = this.radius * 2,
    maxH = diam > this.textReact.height ? diam : this.textReact.height; //矩形的高度
  return {
    x,
    y,
    minX: x,
    maxX: this.center.x + this.radius,
    minY: this.center.y - maxH / 2,
    maxY: this.center.y + maxH / 2
  };
}

以此类推,描述下面、左面、上面的虚拟矩形坐标。

判断碰撞

判断两个矩形是否覆盖相交,根据矩形的 minX,maxX,minY,maxY 判断相交,原理比较简单,代码如下:

/**
 * 判断分位是否相交
 * @param {*} target 
 */ 
isAnchorMeet(target) {
  let react = this.getCurrentRect(),
    targetReact = target.getCurrentRect();
  if ((react.minX < targetReact.maxX) && (targetReact.minX < react.maxX) &&
    (react.minY < targetReact.maxY) && (targetReact.minY < react.maxY)) {
    return true;
  }
  return false;
}

创建虚拟文字集合对象

let labels = pixels.map((val) => {
  let radius = val.pixel.radius + this.style.normal.borderWidth; //圆点半径
  return new Label(val.pixel.x, val.pixel.y, radius, fontSize, byteWidth, val.name);
});

递归遍历虚拟文字集合、判断是否与其他相交,如果有相交就移动当前文字位子,直到不相交为止。当找不到合适位置时,就选择隐藏当前文字。

代码如下:

do {
  var meet = false; //本轮是否有相交
  for (let i = 0; i < labels.length; i++) {
    let temp = labels[i];
    for (let j = 0; j < labels.length; j++) {
      if (i != j && temp.show && temp.isAnchorMeet(labels[j])) {
        temp.next();
        meet = true;
        break;
      }
    }
  }
} while (meet);

绘画文字

labels.forEach(function (item) {
  if (item.show) { //是否显示
    let pixel = item.getCurrentRect();
    ctx.beginPath();
    ctx.fillText(item.text, pixel.x, pixel.y);
    ctx.fill();
  }
});

文字避让算法到目前介绍完了,对应的 inMap 文件地址为https://github.com/TalkingData/inmap/blob/master/src/worker/helper/Label.js,接下来还会继续给大家分享干货。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
Jquery 实现Tab效果 思路是js思路
Mar 02 Javascript
display和visibility的区别示例介绍
Feb 26 Javascript
JavaScript设置body高度为浏览器高度的方法
Feb 09 Javascript
AngularJS基础 ng-options 指令详解
Aug 02 Javascript
浅谈JavaScript的自动垃圾收集机制
Dec 15 Javascript
使用JavaScript触发过渡效果的方法
Jan 19 Javascript
Vue 2.0中生命周期与钩子函数的一些理解
May 09 Javascript
Node.js爬取豆瓣数据实例分析
Mar 05 Javascript
JQuery选中select组件被选中的值方法
Mar 08 jQuery
js实现简单模态框实例
Nov 16 Javascript
vue + typescript + video.js实现 流媒体播放 视频监控功能
Jul 07 Javascript
JS+html5实现异步上传图片显示上传文件进度条功能示例
Nov 09 Javascript
关于HTTP传输中gzip压缩的秘密探索分析
Jan 12 #Javascript
用最少的JS代码写出贪吃蛇游戏
Jan 12 #Javascript
Javascript将图片的绝对路径转换为base64编码的方法
Jan 11 #Javascript
如何去除vue项目中的#及其ie9兼容性
Jan 11 #Javascript
全新打包工具parcel零配置vue开发脚手架
Jan 11 #Javascript
详解VUE2.X过滤器的使用方法
Jan 11 #Javascript
Vuex提升学习篇
Jan 11 #Javascript
You might like
php合并数组array_merge函数运算符加号与的区别
2008/10/31 PHP
无法在发生错误时创建会话,请检查 PHP 或网站服务器日志,并正确配置 PHP 安装最快的解决办法
2010/08/01 PHP
PHP设计模式之迭代器模式
2016/06/17 PHP
TP5框架实现的数据库备份功能示例
2020/04/05 PHP
漂亮的仿flash菜单,来自蓝色经典
2006/06/26 Javascript
Prototype使用指南之string.js
2007/01/10 Javascript
用js实现控制内容的向上向下滚动效果
2007/06/26 Javascript
使用jQuery模板来展现json数据的代码
2010/10/22 Javascript
js实现杯子倒水问题自动求解程序
2013/03/25 Javascript
table行随鼠标移动变色示例
2014/05/07 Javascript
超级给力的JavaScript的React框架入门教程
2015/07/02 Javascript
JQuery日期插件datepicker的使用方法
2016/03/03 Javascript
JavaScript中的boolean布尔值使用学习及相关技巧讲解
2016/05/26 Javascript
JS中跳出循环的示例代码
2017/09/14 Javascript
jQuery中extend函数简单用法示例
2017/10/11 jQuery
微信小程序实现打开并下载服务器上面的pdf文件到手机
2019/09/20 Javascript
ElementUI多个子组件表单的校验管理实现
2019/11/07 Javascript
ant-design表单处理和常用方法及自定义验证操作
2020/10/27 Javascript
[01:24]2014DOTA2 TI第二日 YYF表示这届谁赢都有可能
2014/07/11 DOTA
[01:10]DOTA2亚洲邀请赛 征战号角响彻全场
2015/01/06 DOTA
[15:20]DOTA2亚洲邀请赛总决赛开幕式表演:羽泉献唱
2017/04/05 DOTA
python列表与元组详解实例
2013/11/01 Python
python的unittest测试类代码实例
2017/12/07 Python
python3+PyQt5使用数据库窗口视图
2018/04/24 Python
基于python分析你的上网行为 看看你平时上网都在干嘛
2019/08/13 Python
Python定时任务随机时间执行的实现方法
2019/08/14 Python
日本面向世界,国际级的免税在线购物商城:DOKODEMO
2017/02/01 全球购物
Sasa莎莎海外旗舰店:香港莎莎美妆平台
2018/03/21 全球购物
EMPHASIS艾斐诗官网:周生生旗下原创精品珠宝品牌
2020/12/17 全球购物
New delete 与malloc free 的联系与区别
2013/02/04 面试题
关于.NET, HTML的五个问题
2012/08/29 面试题
《中国梦我的梦》中学生演讲稿
2014/08/20 职场文书
公安纪律作风整顿剖析材料
2014/10/10 职场文书
出纳岗位职责范本
2015/03/31 职场文书
JS异步堆栈追踪之为什么await胜过Promise
2021/04/28 Javascript
使用 Apache Superset 可视化 ClickHouse 数据的两种方法
2021/07/07 Servers