如果不懂地图聚合点,可以先看这个文章,了解各个算法:地图兴趣点聚合算法的探索与实践。
作为giser可能首先会考虑直接用 openlayers 或 cesium 实现点位聚合,但是面对庞大数据,显然 openlayers 和 cesium 都太吃性能了。
简单介绍一下方案:将经纬度根据不同地图层级切割成一个个独立的小格子,类似以上链接中的这个图,不过实现过程跟他讲的不一样。反正结果就是每个格子的点聚合到一起。
框架:redis 存储基础点和聚合点,go 将基础点算成聚合点,web 显示点位。
redis
点(经纬度)存在数据库中的,全取出来利用redis做聚合运算,因为 redis 的读/写性能远超数据库。
首先要了解 redis 的数据类型及命令语句,看官网就可以了。
这里需要用到两个redis服务器,为了区分,一个叫 infoRes,一个叫 geoRes。infoRes 中存基础点和聚合点的所有详细信息,数据类型用的是 Hash,geoRes 根据不同层级存聚合点位置,另外基础点也存在这里面。具体用法继续看 Go。
Go
点位计算存储
- 从数据库里取出点后要把无效的经纬度筛掉,比如空的、不符合经纬度范围的等等。
- 获取地图层级数组。
- 每个层级下,分批获取 geoRes 中的基础点,主要是拿到每个点的 key。
- 拿 key 去 infoRes 去拿每个点的详细信息。
- 根据当前层级、经纬度,得出当前点所在格子的序列号 clusterKey。
- 这一步就在算聚合点的位置和数量了,有相同 clusterKey 的算一个格子,数量加起来,经纬度要根据数量比例算。
代码有简化,仅供参考
buffer := make(map[string]*clusterPoint)
for i:= 0; ; i++ {
// 分批获取 geoRes 中的基础点,主要是拿到每个点的 key
arr, _ := redis.String(geoRes.Do("ZREVRANGEBYSCORE", "marker", "+inf", "-inf", "limit", i*1000, 1000 ))
if arr == nil || len(arr) == 0 {
break
}
for _, temp := range arr {
// 拿 key 去 infoRes 去拿每个点的详细信息
info, _ := redis.Strings(infoRes.Do("HGETALL", key))
dic := make(map[string]string, len(info)/2)
for i := 0; i < len(info); i = i + 2 {
dic[info[i]] = info[i+2]
}
// 根据层级、经纬度序列化一个聚合缓存 clusterKey,表示哪个格子
clusterKey := encodeClusterKey(dic["lng"], dic["lat"], currentZoom)
item := buffer[clusterKey]
if item == nil {
item = &clusterPoint{}
……
item.num, _ = strconv.Atoi(dic["num"])
item.clusterKey = clusterKey
item.lng, _ := strconv.ParseFloat(dic["lng"], 64)
item.lat, _ := strconv.ParseFloat(dic["lat"], 64)
……
buffer[clusterKey] = item
}
// 数量加起来,经纬度要根据数量比例算
item.lng = (item.lng * float64(item.num) + lng) / float64(item.num + 1)
item.lat = (item.lat * float64(item.num) + lat) / float64(item.num + 1)
item.num++
}
}
// 聚合点存入 geoRes
for key, value := range buffer {
// 根据 clusterKey 算出一个格子的标准点,这个点只是参考位置
centerLng, centerLat := encodeClusterKey(value.clusterKey)
// 根据自己的需要弄个新的 key 出来,作为 redis geo 的 member,把 clusterKey 放进去
tempKey := ……
// 分层级往 geoRes 存聚合点位
_, err := geoRes.Do("GEOADD", fmt.Sprintf("cluster%d", currentZoom), centerLng, centerLat, tempKey)
// 往 infoRes 存点位具体信息
_, err = infoRes.Do("HMSET", tempKey, ……)
}
获取点位
- 前端传来地图层级、视图中心点的经纬度、视图范围
- 拿到聚合点后不能直接给前端,要优化点的位置,避免点位重叠
tempArr, _ := redis.Strings(geoRes.Do("GEORADIUS", fmt.Sprintf("cluster%d", currentZoom), lng, lat, distance, "m"))
for i:= 0; i < len(tempArr); i++ {
info, _ := redis.Strings(infoRes.Do("HGETALL", tempArr[i]))
dic := make(map[string]string, len(info)/2)
for i := 0; i < len(info); i = i + 2 {
dic[info[i]] = info[i+2]
}
item = &clusterPoint{}
……
item.num, _ := strconv.Atoi(dic["num"])
item.lng, _ := strconv.ParseFloat(dic["lng"], 64)
item.lat, _ := strconv.ParseFloat(dic["lat"], 64)
……
}
web
前端不能每次都靠后台传值刷新,毕竟点太多了,每次都全量刷新肯定需要一定时间加载,用户体验也很不好。所以前端需要缓存一份点位数据,每次获得新数据时先比较缓存,比较相同的聚合点,如果其中数量没变就不更新,数量变了就删掉重新渲染。
大概就是这样的思路,原创文章,转载请注明出处。