自定义Label标签抽稀(openlayers)

        在gis开发中,经常碰到的一个问题就是抽稀的问题。在openlayers的高版本里,例如ol.layer.Vector类已经有declutter参数,只要设置为true,就可以执行抽稀的操作(其内部使用的是RBush,参考openlayers中RBush使用)。但是今天要说的是一个具体的需求,也比较常见,就是标签抽稀的问题,使用openlayers自带的declutter并不能完美的解决。

        需求是这样:有一堆的点坐标,可能在地图上显示为各种图标,并且我们需要在它旁边显示一个label标签,显示它的名字或者一些其他的信息,当label过多,在各个分辨率下都需要很好的显示,因为缩小地图时,标签叠在一起很难看不说,还很影响性能。一般来说,处理的方式就是将标签抽稀,但是在这个基础上,还需要将标签位置做一下移动的操作,比如说将图标作为原点,标签显示在其周围360度,为什么要这么做呢,是因为要尽可能利用好空间,如果只将label放到目标图标的右边,那么其上边,左边下边都可能还有空位,图标一多,发现还有很多空位其实是可以显示label的,所以在不同分辨率下就需要动态计算label的位置,一般label的位置我们可以取目标坐标(图标)的右上、上边、左上、左边、左下、下边、右下、右边。在各种不同的分辨率下重新计算label的位置即可。

        第二个值得注意的是怎么判断哪些标签是需要显示的,哪些是需要隐藏的,很简单,我们只需要遍历这上述的8个位置即可,判断是否与已经显示的标签碰撞,如果不碰撞,就标记为显示,并计算出label的位置,放入显示的标签的set中,给后来的label提供计算的依据。

        怎么判断两个矩形是否碰撞呢,只需要判断一个矩形是否是在另一个矩形的上边或者右边即可(这样就是不碰撞)

        好了,接下来话不多说,贴上具体的代码实现。

import { every } from 'lodash'

export class TextLabel {
  constructor(param) {
    this.map = param.map
    this.text = param.text
    this.geom = param.geom
    this.padding = param.padding || [0, 0, 0, 0]
    this.fontStyle = param.fontStyle || '12px Microsoft YaHei'

    // 下面为计算出来的属性
    this.show = false
    this.offsetX = null
    this.offsetY = null
    // 最终位置
    this.finalPosition = null
    // 参考点,连线终点
    this.referencePoint = null

    this.computeDimensions()
    // 文字标签的初始位置
    this.computePosition(this.geom)
  }

  computePosition(geom) {
    var g1 = this.map.WKT.readGeometry(geom).getCoordinates()
    //获取屏幕坐标
    var pixelPoint = this.map.getPixelFromCoordinate(g1)
    this.position = [Math.round(pixelPoint[0]), Math.round(pixelPoint[1])]
  }

  computeDimensions() {
    // 如果宽度和高度已经计算过,则直接返回它们
    if (this.width && this.height) {
      return
    }

    // 如果宽度和高度未计算,则使用复用的临时DOM元素进行计算
    if (!TextLabel.textDimensionsCache) {
      // 创建临时的DOM元素,用于计算文本尺寸
      TextLabel.textDimensionsCache = document.createElement('span')
      TextLabel.textDimensionsCache.style.visibility = 'hidden'
      TextLabel.textDimensionsCache.style.whiteSpace = 'nowrap'
      TextLabel.textDimensionsCache.style.position = 'absolute'
      TextLabel.textDimensionsCache.style.left = '0'
      TextLabel.textDimensionsCache.style.top = '0'
      document.body.appendChild(TextLabel.textDimensionsCache)
    }

    TextLabel.textDimensionsCache.style.font = this.fontStyle
    TextLabel.textDimensionsCache.innerText = this.text
    const dimensions = TextLabel.textDimensionsCache.getBoundingClientRect()
    this.width = dimensions.width
    this.height = dimensions.height
  }
}

export class TextDeclutter {
  constructor(param = {}) {
    this.map = param.map
    // 此值为true时,计算一次后会颠倒 directions 的遍历方向,保证均匀分布
    this.reverse = param.reverse
    this.offsetX = param.offsetX || 12
    this.offsetY = param.offsetY || 12
    // 右上、右下、左下、左上四个位置偏移
    // 第一个值为默认值
    this.directions = [
      [1, -1],
      [1, 1],
      [-1, 1],
      [-1, -1]
    ].map((item) => [item[0] * this.offsetX, item[1] * this.offsetY])
    // 下面这两个外边距决定了水平,垂直方向上抽稀的间隔,设置大一点标签就少一点
    this.marginX = param.marginX || 30
    this.marginY = param.marginY || 30

    this.reset()
  }

  reset() {
    this.labels = []
  }

  calcPos(label) {
    if (this.labels.indexOf(label) === -1) {
      this.labels.push(label)
    }
    if (this.reverse) {
      this.directions = this.directions.reverse()
    }
    for (const direction of this.directions) {
      let ox = direction[0]
      let oy = direction[1]
      let rpx = label.position[0] + ox
      let rpy = label.position[1] + oy
      // text基线是
      // textBaseline: 'bottom',
      // textAlign: 'left'
      let [fx, fy] = this.offsetPos(label.position, direction)
      if (ox < 0) {
        fx = fx - label.width
        ox = ox - label.width - label.padding[3]
      } else {
        ox = ox + label.padding[1]
      }
      if (oy > 0) {
        fy = fy + label.height
        oy = oy + label.height + label.padding[0]
      } else {
        oy = oy - label.padding[2]
      }

      label.finalPosition = [fx, fy]
      label.offsetX = ox
      label.offsetY = oy
      label.referencePoint = [rpx, rpy]

      const noOneIntersect = every(this.labels, (lb) => {
        if (lb === label) {
          return true
        }
        return this.noIntersect(label, lb)
      })
      if (noOneIntersect) {
        label.show = true
        return label
      }
    }
    // 执行到这里,说明所有的位置都有相交,不显示此标签
    label.show = false
    label.finalPosition = null
    return label
  }

  offsetPos(originPos, offset) {
    return originPos.map((item, i) => item + offset[i])
  }

  // 判断两矩形是否不相交
  noIntersect(label1, label2) {
    if (!label1.finalPosition || !label2.finalPosition) {
      return true
    }
    // 检查两个标签是否相交
    // 如果一个矩形在另一个矩形的右边或上面,那么它们不会重叠
    return (
      label1.finalPosition[0] > label2.finalPosition[0] + label2.width + this.marginX ||
      label2.finalPosition[0] > label1.finalPosition[0] + label1.width + this.marginX ||
      label1.finalPosition[1] > label2.finalPosition[1] + label2.height + this.marginY ||
      label2.finalPosition[1] > label1.finalPosition[1] + label1.height + this.marginY
    )
  }
}

使用流程:

  1. 全局初始化一个TextDeclutter对象
  2. 每次绘制所有图标前执行TextDeclutter.reset()方法,清空下内部计算缓存
  3. 每个label都使用TextLabel构建对象,并使用TextDeclutter的calcPos(label)方法进行计算
  4. 使用label对象里已经计算好的是否显示、位置、连接点等信息,在地图上绘制你的图标即可

其中判断碰撞我们是用遍历检测,在大量数据的时候效率不行。可以使用RBush优化,参考openlayers中RBush使用 

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值