前言
最近做了一个 echarts 地图聚合的功能,因为项目没办法用百度地图的聚合api,所以只得手写。在这个过程中,也走了不少弯路,所幸每一步都算数。
什么是地图聚合点
把地图上经纬度相近的点聚合成一个点,用户点击这个点,地图放大,可以看到聚合的那几个点;地图缩放,聚合点也随之变化。可以想象一下散点图。
与地图聚合点相关的计算
既然要把多个相近的点聚合成一个点,那就得有一个距离来判断这个几个点能否聚合一起,这就涉及到两点之间的距离计算。
1.计算两点之间的距离
关于地图上,两点之间的距离计算,网上有很多,在这里我用的是两点距离计算公式。
const calDistance = (p1, p2) => {
return Math.sqrt(Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2))
}
// ◆测试用例
console.log(calDistance([0, 0], [3, 4])) // 5
2.计算聚合点(核心)
const calMapPoints = (mainList, dis) => {
let MapList = [] //存放聚合的点
let index = 1
for (let i = 0; i < mainList.length; i++) {
// 数组里面 push 空数组,用于存放 mainList 对应的聚合点
MapList.push([])
}
// 轮循就计算两两之间的距离,如果小于等于阈值,则 push 进去
for (let i = 0; i < mainList.length; i++) {
index = index == i ? i + 1 : index
for (let j = index; j < mainList.length; j++) {
let tempDis = calDistance(mainList[i].value, mainList[j].value)
if (tempDis <= dis) {
MapList[i].push(mainList[j])
}
index = j + 1
}
}
// 计算 mainList 中对应索引的聚合值,并把聚合的数组 push 进去,知道这个点聚合了哪几个点
MapList.forEach((item, index) => {
if (item.length != 0) {
mainList[index].total = item.length
mainList[index].pointArr = [...item]
}
})
// 循环 MapList,将 mainList 包含的 MapList 对应索引的值删除掉,只保留聚合的那个点
MapList.forEach(item => {
item.forEach(it => {
let ind = mainList.indexOf(it)
mainList.splice(ind, 1)
})
})
return mainList
}
// ◆测试用例
let arr = [
{ value: [1, 2] },
{ value: [2, 4] },
{ value: [4, 3] },
{ value: [3, 4] }
]
console.log(calMapPoints(arr, 3))
// [
// { value: [ 1, 2 ], total: 2, pointArr: [ { value: [2, 4] }, { value: [3, 4] } ] },
// { value: [ 4, 3 ] }
// ]
3.计算聚合的几个点的中心经纬度(中心点)与放大倍数
我们点击聚合点,需要收集被聚合的那几个点,包括自己
const getAllPointArr = (totalPoint) => {
let currPoint = {latitude: totalPoint.value[0],longitude: totalPoint.value[1]}
let otherPoint = []
totalPoint.pointArr.forEach(item => {
let objPoint = {latitude: item.value[0],longitude: item.value[1]}
otherPoint.push(objPoint)
})
let allPointArr = [currPoint, ...otherPoint]
return allPointArr
}
// 测试用例
let totalPoint = { value: [1, 2], total: 2, pointArr: [{ value: [2, 4] }, { value: [3, 4] }] }
console.log(getAllPointArr(totalPoint))
// [
// { latitude: 1, longitude: 2 },
// { latitude: 2, longitude: 4 },
// { latitude: 3, longitude: 4 }
// ]
然后我们需要计算此时地图的中心点
const getCenterPoint = (allPointArr) => {
// 如果有则拍平,无则不变
const arrFlat = allPointArr.reduce((s, v) => {
return (s = s.concat(v))
}, [])
const total = arrFlat.length
let X = 0
let Y = 0
let Z = 0
for (let key of arrFlat) {
const latitude = key.latitude * Math.PI / 180
const longitude = key.longitude * Math.PI / 180
const x = Math.cos(latitude) * Math.cos(longitude)
const y = Math.cos(latitude) * Math.sin(longitude)
const z = Math.sin(latitude)
X += x
Y += y
Z += z
}
X = X / total
Y = Y / total
Z = Z / total
const lon = Math.atan2(Y, X)
const hyp = Math.sqrt(X * X + Y * Y)
const lat = Math.atan2(Z, hyp)
return [lon * 180 / Math.PI, lat * 180 / Math.PI]
}
// 测试用例
let allPointArr = [
{ latitude: 1, longitude: 2 },
{ latitude: 2, longitude: 4 },
{ latitude: 3, longitude: 4 }
]
console.log(getCenterPoint(allPointArr))
// [ 3.3329909035534735, 2.0002706541364237 ]
那如何计算放大倍数呢?这里采用的是:计算聚合点两两之间的距离,用最小距离乘以一个整数,这个整数自己定义
const getMinDisArr = (totalPoint) => {
let currPoint = totalPoint.value
let otherPoint = totalPoint.pointArr.map(item => item.value)
let allPointArr = [currPoint, ...otherPoint]
let disArr = []
for (let i = 0; i < allPointArr.length - 1; i++) {
for (let j = i + 1; j < allPointArr.length; j++) {
let dis = calDistance(allPointArr[i], allPointArr[j])
disArr.push(dis)
}
}
let minDis = Math.min(...disArr)
return { minDis, disArr }
}
// 测试用例
let totalPoint = { value: [1, 2], total: 2, pointArr: [{ value: [2, 4] }, { value: [3, 4] }] }
console.log(getMinDisArr(totalPoint))
// { minDis: 1, disArr: [ 2.23606797749979, 2.8284271247461903, 1 ] }
4.地图缩放该如何聚合
注意,地图缩放的时候,经纬度是不变的,那么我们计算的聚合点也不会发生变化,只得改变阈值dis :地图放大,各个点之间的距离变大,地图缩小,各个点之间的距离变小。
一开始我也是按照这个思路来处理地图的缩放事件georoam,但是这个距离是非常难调,除非你的各个点之间的经纬度区别不大,或者非常一致。可惜的是,我的项目各个点的经纬度天差地别,众口难调吧。后来经历种种波折,终于搞明白了:
首先我们要想一个问题,地图缩放,经纬度没有改变,那改变的是什么呢?像素值呀!所以,只要我们获取了地图缩放时经纬度对应点的像素值,那我们设定的阈值 dis 就可以保持不变啦!这就涉及到一个很重要的知识点,echarts 地图的 convertToPixel 方法,经纬度转像素。那么,我们在计算距离的时候,把经纬度转一下不就好啦!
const calDistance = (p1, p2, myChart) => {
// 将地图经纬度转为像素,地图缩放,像素变化
let px1 = myChart.convertToPixel('geo', p1)
let px2 = myChart.convertToPixel('geo', p2)
return Math.sqrt(Math.pow(px1[0] - px2[0], 2) + Math.pow(px1[1] - px2[1], 2))
}
结语
我在做这个地图聚合功能的过程中,查过网上很多的资料,参差不齐。我一开始的方向的让阈值变化,导致缩放聚合功能卡了许久,老是达不到满意的效果。结果,把经纬度转像素就迎刃而解了。有时候真不能盲目的借鉴,得思考。