你不知道的前端算法之文字避让

本文做者:TalkingData 可视化工程师李凤禄javascript

编辑:Aresn前端

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

GitHub 地址:github.com/TalkingData…react

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

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

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

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

下面展现的是 inMap 文字避让效果:算法

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

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;
复制代码

文字的宽度和高度获得后,咱们就能够建立文字矩形的坐标系了。

建立四分位模型

image

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

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

image

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

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

代码以下:

_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 文件地址为github.com/TalkingData…,接下来还会继续给你们分享干货。

福利

分享两位业内大牛的前端课程:

相关文章
相关标签/搜索