前言
百度地图jsapi的文档不全,且零散,看到别人对于高德地图海量标注卡顿的解决思路的文章,延续着做了百度地图的版本,并从中得知百度地图的两个库MapVGL和BMapGLLib,并利用这两个库来对百度地图版的海量标注做卡顿优化处理。
涉及库
MapVGL
百度地图官方提供,是一款基于WebGL的地理信息可视化库,可以用来展示大量基于3D的地理信息点线面数据。设计初衷主要是为了解决大数据量的三维地理数据展示问题及一些炫酷的三维效果。
热力图
点聚合
BMapGLLib
百度地图JSAPI GL版JavaScript开源工具库
DrawingManager
提供鼠标绘制点、线、面、多边形(矩形、圆)的编辑工具条的开源代码库。且用户可使用JavaScript API对应覆盖物(点、线、面等)类接口对其进行属性(如颜色、线宽等)设置、编辑(如开启线顶点编辑等)等功能。
RichMarker
百度地图的富Marker类,对外开放。 允许用户在自定义丰富的Marker展现样式,并添加点击、双击、拖拽等事件。
除此之外还有很多方便使用的工具库,只是没有文档:
GeoUtils:计算点是否在某个圆形、矩形、多边形区域内
DistanceTool: 测距工具,用于测距离
案例
海量点标记
海量点标记+海量标注 地图卡顿问题,高德地图源码是 yingjieweb 这位作者的,突发奇想基于他的文章做相对应的百度地图对比。
官方原话:
关于「海量标注」,高德地图的官方解释是:当需要在地图添加千级以上的点标记时,LabelMarker 是代替 Marker 的更好选择。不同于 MassMarks ,LabelMarker 不仅可以绘制图标,还可以为图标添加文字信息,且万级以上数据也具有较好性能,配置也更加灵活,但是其实性能并没有那么好,当数据量达到三千级以上的时候,LabelMarker 的画面流畅程度大大缩减了,基本上是用户所不能接受的地步,因此我们需要对其进行优化。
初始化一个百度地图
导入jsapi自动引入的库:bmap-jsapi-loader
import * as BMapLoader from '@/assets/lib/bmap-jsapi-loader.js'
BMapLoader.loader({
v: '1.0',
ak: 'EAcsTtlRiGlDRKhCPbuofCcH9agP9gSQ',
type: 'webgl',
library: [{ //导入扩展库 BMapGLLIB
lib: 'GeoUtils'
}, {
lib: 'RichMarker'
}, {
lib: 'InfoBox'
}]
}).then(() => {
map = new BMapGL.Map("map")
map.centerAndZoom(new BMapGL.Point(114.036847, 22.526414), 14)
map.enableScrollWheelZoom()
isLoading.value = false
setMassMarks(); // 设置海量点标记 marker
setMapListener(); // 监听地图 缩放/移动 事件
})
优化步骤
针对高德地图版海量标注改造的百度地图版本
1.针对首屏加载速度的优化:
页面一进入的时候看到的是「海量点标记」的图层,放大的时候再展示「海量标注」的图层
{
"id": 18678,
"projName": "南澳湾花园",
"cityId": 197,
"districtId": 28835,
"districtName": "大鹏新区",
"streetName": "南澳",
"location": "114.49774333354895,22.537202039488108",
"price": 18000,
"level": 1,
}
const setMassMarks = () => {
// 设置多个图标
const styleObjectArr = [
new BMapGL.Icon('https://t1.focus-img.cn/applet/2022-04-25/a7830c321d0842dea1ce772f622fce02.png', new BMapGL.Size(30, 30)),
new BMapGL.Icon('https://t1.focus-img.cn/applet/2022-04-25/721f4aee79ed4a3299fee8692b51c81c.png', new BMapGL.Size(30, 30)),
new BMapGL.Icon('https://t1.focus-img.cn/applet/2022-04-25/37df30680cce4b41b361a6b681edb9b0.png', new BMapGL.Size(30, 30)),
];
// 遍历房子
houseList.forEach((item) => {
// location: 114.49774333354895,22.537202039488108"
let lnglat = item.location.split(",")
let overlay = new BMapGL.Marker(new BMapGL.Point(...lnglat), {
icon: styleObjectArr[item.level - 1]
})
overlay.setZIndex(item.level)
map.addOverlay(overlay) //绘制
});
};
let isMassMark = false
let zoom;
map.on("zoomend", () => {
// 监听地图缩放事件 -> 执行条件渲染
zoom = map.getZoom();
if (zoom >= 15) {
isMassMark = true
clearMark()
executeConditionRender();
}
else if(zoom<15 && isMassMark) {
isMassMark = false
clearMark()
setMassMarks()
}
});
map.on("moveend", () => {
// 监听地图移动事件 -> 执行条件渲染
if (zoom >= 15) {
isMassMark = true
map.clearOverlays()
executeConditionRender();
} else if(zoom<15 && isMassMark) {
isMassMark = false
map.clearOverlays()
setMassMarks()
}
});
2.针对海量标注的分片加载优化
检测「海量标注」中的数据项,判断其坐标是否在浏览器视口区域,从而进行分片渲染:
可以通过getBounds() API获取视口中西北和东南的坐标点,然后可以通过组合得到四个角的坐标点,再一次区判断某个点是否在这个区域内,在就渲染,从而实现每次拖拽后,根据视口渲染局部数据,保证页面性能
let screenCoordinateRange = map.getBounds()
let northEast = [
screenCoordinateRange.ne.lng,
screenCoordinateRange.ne.lat,
];
let southEast = [
screenCoordinateRange.sw.lng,
screenCoordinateRange.ne.lat,
];
let southWest = [
screenCoordinateRange.sw.lng,
screenCoordinateRange.sw.lat,
];
let northWest = [
screenCoordinateRange.ne.lng,
screenCoordinateRange.sw.lat,
];
但是这种手写的比较麻烦,可以使用BMapGLLIB.GeoUtils库,里面提供了
- 判断点是否在矩形内
- 判断点是否在圆形内
- 判断点是否在折线上
- 判断点是否多边形内
- 计算两点之间的距离
- 计算折线或者点数组的长度
- 判断折线与多边形是否相交
过滤视口的所有房源数据
// 视口 西北、东南坐标
let screenCoordinateRange = map.getBounds()
// 得到视口中所有房子信息
let screenHouseList = houseList.filter((item) => {
let lnglat = item.location.split(",")
let point = new BMapGL.Point(...lnglat)
return BMapGLLib.GeoUtils.isPointInRect(point, screenCoordinateRange);
});
渲染信息
BMapGLLib.InfoBox: 百度地图的infoBox。类似于infoWindow,比infoWindow更有灵活性,比如可以定制border,关闭按钮样式等。
screenHouseList.map((item) => {
let point = new BMapGL.Point(...item.location.split(","))
let marker = new BMapGL.Marker(point)
let html = `<div class="amap-info-window__${priceClassArr[item.level - 1]}">
<div class="amap-info-title">${item.projName}</div>
<div class="amap-info-price">${item.price}元/平方米</div>
</div>` bv
let infoBox = new BMapGLLib.InfoBox(map, html)
map.addOverlay(marker)
infoBox.open(marker)
labelInfoMarks.push(infoBox) //储存信息窗口,后面调用close方法关闭
});
即使可以通过一些方法来减少渲染的时间,但是还是比较麻烦,而且在百度地图中,默认的标点都是dom元素去实现事件驱动的,获得更好的性能可以使用MapVGL矢量图层。
使用MapVGL优化:
使用MapVGL矢量图层,分图层,相同的元素放在同一个图层上,使用多个canvas去渲染,实现图层切换是需要显示隐藏某个canvas图层,使用数据驱动GeoJSON
GeoJSON
mapvgl主要是用来展示地理信息数据的,传入可视化图层的数据格式需要具有geometry字段来定义坐标信息,同时也可能会需要properties字段来携带一些和该坐标绑定的属性,geometry和properties字段的内容统一使用GeoJSON的规范
// 点数据
[{ geometry: { type: 'Point', coordinates: [116.392394, 39.910683]
},
properties: {
color: 'rgba(255, 255, 50, 0.5)',
count: 90
}
}]
// 线数据
[{ geometry: { type: 'LineString', coordinates: [ [116.392394, 39.910683],
[116.423123, 39.134534]
]
},
properties: {
count: 30
}
}]
// 面数据
[{ geometry: { type: 'Polygon', coordinates: [ [ [112.342124, 38.333312],
[116.392394, 39.910683],
[116.423123, 39.134534]
]
]
},
properties: {
count: 30 * Math.random()
}
}]
基本使用
<script src="https://unpkg.com/mapvgl/dist/mapvgl.min.js"></script>
或
npm i mapvgl
1.初始化
import * as mapvgl from 'mapvgl'
BMapLoader.loader({
v: '1.0',
ak: 'EAcsTtlRiGlDRKhCPbuofCcH9agP9gSQ',
type: 'webgl',
library: [{
lib: 'GeoUtils'
}, {
lib: 'RichMarker'
}, {
lib: 'InfoBox'
}]
}).then(() => {
//1.创建地图示例
map = new BMapGL.Map("map")
map.centerAndZoom(new BMapGL.Point(114.036847, 22.526414), 14)
map.enableScrollWheelZoom()
//2.创建MapVGL图层管理器
view = new mapvgl.View({
map
});
})
2.绘制海量点
// 设置多个图标
/**
const styleObjectArr = [
new BMapGL.Icon('https://t1.focus-img.cn/applet/2022-04-25/a7830c321d0842dea1ce772f622fce02.png', new BMapGL.Size(30, 30)),
new BMapGL.Icon('https://t1.focus-img.cn/applet/2022-04-25/721f4aee79ed4a3299fee8692b51c81c.png', new BMapGL.Size(30, 30)),
new BMapGL.Icon('https://t1.focus-img.cn/applet/2022-04-25/37df30680cce4b41b361a6b681edb9b0.png', new BMapGL.Size(30, 30)),
];
let data = houseList.map((item) => {
let lnglat = item.location.split(",")
let point = new BMapGL.Point(...lnglat)
let overlay = new BMapGL.Marker(point, {
icon: styleObjectArr[item.level - 1]
})
overlay.setZIndex(item.level)
map.addOverlay(overlay)
})
*/
// 准备不同的图片
const picArr = [
'https://t1.focus-img.cn/applet/2022-04-25/a7830c321d0842dea1ce772f622fce02.png',
'https://t1.focus-img.cn/applet/2022-04-25/721f4aee79ed4a3299fee8692b51c81c.png',
'https://t1.focus-img.cn/applet/2022-04-25/37df30680cce4b41b361a6b681edb9b0.png'
]
// 处理数据
let data = houseList.map(item=>{
return {
geometry:{
type:"Point",
coordinates:item.location.split(',')
},
properties:{
level:item.level
}
}
}).sort(a,b=>a.properties.level - b.properties.level)
模拟数据
[{ "id": 18678, "projName": "南澳湾花园", "cityId": 197, "districtId": 28835, "districtName": "大鹏新区", "streetName": "南澳", "location": "114.49774333354895,22.537202039488108", "price": 18000, "level": 1,}]
创建图层
layer = new mapvgl.IconLayer({
width:40,
height: 40,
opacity: 1, //透明度
icon: function ({properties}){
return styleObjectArr[properties.level - 1]
},
blend:'lighter' //颜色叠加模式
});
view.addLayer(layer); //将图层交给图层管理器去管理
layer.setData(data); //为图层设置数据
3.绘制海量标记
BMapGLLib.InfoBox: 百度地图的infoBox。类似于infoWindow,比infoWindow更有灵活性,比如可以定制border,关闭按钮样式等。
BMapGLLib.RichMarker: 同InfoBox一样,也是做富文本标签信息窗格的,只是他不会绘制点,只绘制infoWindow
let data = screenHouseList.map(item=>{
let htm = `<div class="amap-info-window__${priceClassArr[item.level - 1]}">
<div class="amap-info-title">${item.projName}</div>
<div class="amap-info-price">${item.price}元/平方米</div>
</div>`
let point = new BMapGL.Point(...item.location.split(","))
// 使用RichMarker来绘制信息窗格
let myRM = new BMapGLLib.RichMarker(htm,point,
{"anchor" : new BMapGL.Size(-56, -60)}
)
// myRM.addEventListener('onclick',function (e){
// console.log(e, 'e')
// })
map.addOverlay(myRM)
// 处理数据
return {
geometry:{
type:"Point",
coordinates:item.location.split(',')
}
}
})
// 创建点图层
labelMarkLayer = new mapvgl.PointLayer({
size: 10
})
view.addLayer(labelMarkLayer)
massMarkLayer.setData(data)
4.图层切换
map.on("zoomend", () => {
// 监听地图缩放事件 -> 执行条件渲染
zoom = map.getZoom();
// 移除海量标记的点图层
if(removeLayer){
view.hideLayer(labelMarkLayer)
}
if (zoom >= 15) {
// 隐藏海量点图层
view.hideLayer(layer)
// 更新信息窗格等数据
executeConditionRender();
}else{
// 显示海量点图层
view.showLayer(layer)
}
});
总结
- 使用矢量点来绘制点标记
- 使用图层来区分不同的点标注,一些不需要改变的点,可以通过隐藏/显示图层来提高性能
- 使用分片来防止一次性全部渲染导致地图节点过多卡顿