你可能不知道的前端算法之文字避让(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 相关文章推荐
根据一段代码浅谈Javascript闭包
Dec 14 Javascript
jquery实现metro效果示例代码
Sep 06 Javascript
js为空或不是对象问题的快速解决方法
Dec 11 Javascript
jquery实现下拉菜单的二级联动利用json对象从DB取值显示联动
Mar 27 Javascript
原生js和jquery实现图片轮播淡入淡出效果
Apr 23 Javascript
jquery实现可自动判断位置的弹出层效果代码
Oct 12 Javascript
浅谈JavaScript事件绑定的常用方法及其优缺点分析
Nov 01 Javascript
js通过classname来获取元素的方法
Nov 24 Javascript
js清除浏览器缓存的几种方法
Mar 15 Javascript
Angular2 之 路由与导航详细介绍
May 26 Javascript
基于JS实现web端录音与播放功能
Apr 17 Javascript
在Express中提供静态文件的实现方法
Oct 17 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中执行系统外部命令
2006/10/09 PHP
PHP判断网络文件是否存在的方法
2015/03/12 PHP
yii2实现根据时间搜索的方法
2016/05/25 PHP
实例讲解YII2中多表关联的使用方法
2017/07/21 PHP
HTML中Select不用Disabled实现ReadOnly的效果
2008/04/07 Javascript
jQuery实现在最后一个元素之前插入新元素的方法
2015/07/18 Javascript
原生js实现图片轮播特效
2015/12/18 Javascript
jQuery formValidator表单验证
2016/01/07 Javascript
node.js+express制作网页计算器
2016/01/17 Javascript
一步步教大家编写酷炫的导航栏js+css实现
2016/03/14 Javascript
BootStrap文件上传样式超好看【持续更新】
2016/05/10 Javascript
AngularJS中的Promise详细介绍及实例代码
2016/12/13 Javascript
JavaScript日期工具类DateUtils定义与用法示例
2018/09/03 Javascript
详解js创建对象的几种方式和对象方法
2021/03/01 Javascript
一个超级简单的python web程序
2014/09/11 Python
Python 出现错误TypeError: ‘NoneType’ object is not iterable解决办法
2017/01/12 Python
python 3.6 tkinter+urllib+json实现火车车次信息查询功能
2017/12/20 Python
Python如何访问字符串中的值
2020/02/09 Python
关于keras中keras.layers.merge的用法说明
2020/05/23 Python
浅谈keras中的目标函数和优化函数MSE用法
2020/06/10 Python
详解python tkinter包获取本地绝对路径(以获取图片并展示)
2020/09/04 Python
详解java调用python的几种用法(看这篇就够了)
2020/12/10 Python
CSS3 制作旋转的大风车(充满童年回忆)
2013/01/30 HTML / CSS
大学毕业的自我鉴定
2013/10/08 职场文书
国贸专业大学生职业生涯规划范文
2014/01/10 职场文书
基督教婚礼主持词
2014/03/14 职场文书
2014年银行柜员工作总结
2014/11/12 职场文书
2014年综合治理工作总结
2014/11/20 职场文书
2015年教师学期工作总结
2015/04/30 职场文书
教师节祝酒词
2015/08/11 职场文书
2016年小学圣诞节活动总结
2016/03/31 职场文书
导游词之金鞭溪风景区
2019/09/12 职场文书
Python数据清洗工具之Numpy的基本操作
2021/04/22 Python
go语言基础 seek光标位置os包的使用
2021/05/09 Golang
如何使用Python提取Chrome浏览器保存的密码
2021/06/09 Python
深入详解JS函数的柯里化
2021/06/09 Javascript