计算地图的聚合点

739 阅读3分钟

前言

最近做了一个 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))
}

结语

我在做这个地图聚合功能的过程中,查过网上很多的资料,参差不齐。我一开始的方向的让阈值变化,导致缩放聚合功能卡了许久,老是达不到满意的效果。结果,把经纬度转像素就迎刃而解了。有时候真不能盲目的借鉴,得思考。