在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
)
}
}
使用流程:
- 全局初始化一个TextDeclutter对象
- 每次绘制所有图标前执行TextDeclutter.reset()方法,清空下内部计算缓存
- 每个label都使用TextLabel构建对象,并使用TextDeclutter的calcPos(label)方法进行计算
- 使用label对象里已经计算好的是否显示、位置、连接点等信息,在地图上绘制你的图标即可
其中判断碰撞我们是用遍历检测,在大量数据的时候效率不行。可以使用RBush优化,参考openlayers中RBush使用