你可能不知道的前端算法之文字避让(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中的常用算法与函数
Nov 21 Javascript
jQuery中获取checkbox选中项等操作及注意事项
Nov 24 Javascript
JS下载文件|无刷新下载文件示例代码
Apr 17 Javascript
jQuery实现自动调整字体大小的方法
Jun 15 Javascript
javascript实现网站加入收藏功能
Dec 16 Javascript
详解javascript跨浏览器事件处理程序
Mar 27 Javascript
Bootstrap Paginator分页插件与ajax相结合实现动态无刷新分页效果
May 27 Javascript
javascript类型系统——undefined和null全面了解
Jul 13 Javascript
JS 验证密码 不能为空,必须含有数字、字母、特殊字符,长度在8-12位
Jun 21 Javascript
详解Vue Elememt-UI构建管理后台
Feb 27 Javascript
Vue中使用JsonView来展示Json树的实例代码
Nov 16 Javascript
uniapp开发小程序的经验总结
Apr 08 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校验ISBN码的函数代码
2011/01/17 PHP
PHP防CC攻击实现代码
2011/12/29 PHP
yii使用activeFileField控件实现上传文件与图片的方法
2015/12/28 PHP
javascript中的if语句使用介绍
2013/11/20 Javascript
用jquery的方法制作一个简单的导航栏
2014/06/23 Javascript
jQuery获取选中内容及设置元素属性的方法
2014/07/09 Javascript
JavaScript设计模式之抽象工厂模式介绍
2014/12/28 Javascript
jQuery使用unlock.js插件实现滑动解锁
2017/04/04 jQuery
vuejs如何配置less
2017/04/25 Javascript
ES6之模版字符串的具体使用
2018/05/17 Javascript
Vue.js实现的表格增加删除demo示例
2018/05/22 Javascript
JavaScript中.min.js和.js文件的区别讲解
2019/02/13 Javascript
微信小程序事件对象中e.target和e.currentTarget的区别详解
2019/05/08 Javascript
发布订阅模式在vue中的实际运用实例详解
2019/06/09 Javascript
Vue 刷新当前路由的实现代码
2019/09/26 Javascript
微信小程序 scroll-view 水平滚动实现过程解析
2019/10/12 Javascript
vue 导航守卫和axios拦截器有哪些区别
2020/12/19 Vue.js
[04:13]2014DOTA2国际邀请赛 专访DC目前形势不容乐观
2014/07/12 DOTA
python版本五子棋的实现代码
2018/12/11 Python
Python创建字典的八种方式
2019/02/27 Python
Opencv+Python实现图像运动模糊和高斯模糊的示例
2019/04/11 Python
Python 实现一个手机号码获取妹子名字的功能
2019/09/25 Python
Django添加bootstrap框架时无法加载静态文件的解决方式
2020/03/27 Python
TensorFlow实现批量归一化操作的示例
2020/04/22 Python
python map比for循环快在哪
2020/09/21 Python
Django生成数据库及添加用户报错解决方案
2020/10/09 Python
用python实现一个简单的验证码
2020/12/09 Python
CSS3之多背景background使用示例
2013/10/18 HTML / CSS
css3实现背景模糊的三种方式(小结)
2020/05/15 HTML / CSS
英国最大的笔记本电脑直销专家:Laptops Direct
2019/07/20 全球购物
彪马法国官网:PUMA法国
2019/12/15 全球购物
护理专业大学生自我推荐信
2014/01/25 职场文书
连锁酒店店长职责范本
2014/02/13 职场文书
药剂专业个人求职信范文
2014/04/29 职场文书
【海涛解说】暗牧也疯狂,牛蛙成配角
2022/04/01 DOTA
MySQL中优化SQL语句的方法(show status、explain分析服务器状态信息)
2022/04/09 MySQL