对于百度地图的基本使用的话,可以看一下其官方文档, 本文着重讲解关于 Marker 标注的使用以及点聚合并结合到地图各省的聚合实现 。
这里会使用到 react-bmapgl 这个库,其是百度地图的 React 封装库,详细使用可以参考其官方文档 。
使用方式和 API 基本和原生一致,只是将控件转换成了组件以及 API 转换成了属性,内部最终还是调用原生的 API 实现。
最终实现效果展示:
前置
先从简单的开始,先看看最简单的 Marker 使用。
数据如下,包括一些基本信息:经纬度、省份代码、名称;后续都会使用该数据进行 Marker 操作。
const markerList = [
{
lng: 121.555858,
lat: 38.976823,
name: '标注1',
provinceCode: '21',
},
{
lng: 121.523497,
lat: 24.945629,
name: '标注2',
provinceCode: '71',
},
{
lng: 121.397243,
lat: 31.174497,
name: '标注3',
provinceCode: '31',
},
{
lng: 121.397243,
lat: 31.174497,
name: '标注4',
provinceCode: '31',
},
{
lng: 116.801042,
lat: 38.94907,
name: '标注5',
provinceCode: '12',
},
{
lng: 104.256265,
lat: 39.99193,
name: '标注6',
provinceCode: '15',
},
{
lng: 122.978436,
lat: 41.130165,
name: '标注7',
provinceCode: '21',
},
{
lng: 108.834663,
lat: 34.398372,
name: '标注8',
provinceCode: '61',
},
{
lng: 121.327012,
lat: 31.200458,
name: '标注9',
provinceCode: '31',
},
{
lng: 117.375726,
lat: 39.133141,
name: '标注10',
provinceCode: '12',
},
]
再来看看地图组件的实现
const Container = () => {
return (
<Map
zoom={6}
center={{ lat: 37.41918484360441, lng: 104.12234245932834 }}
style={{ height: '100%' }}
enableDragging
mapType='normal'
enableScrollWheelZoom
>
{markerList.map(item => (
<Marker key={item.name} position={{ lng: item.lng, lat: item.lat }} />
))}
</Map>
)
}
效果如下:
Map 组件上的 API 属性可以参考以下链接的类参考:
点聚合方案
接下来,我们来看看点聚合。
百度地图本身是不支持点聚合功能的,这里的实现来自扩展内容;我找到了两种实现方式,可以分为两类,简单使用和复杂使用。
MarkerClusterer(简单使用)
官方 Demo:
类参考:
BMapLibrary - BMapLib.MarkerClusterer
这种方式,只是简单的实现点聚合,或者是解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能。
这个在网上有很多实现,并利用它去修改源码实现的一些参考。
但是,这种方式的可扩展性很大,它只有两个源文件,完全可以下下来自行修改其内容,根据自己的需求去调整,但整体来说,过于复杂,只适合简单使用。
@bmapgl-plugin/cluster(复杂使用)
官方 Demo:
这种方式的配置相对来说比较复杂,但功能齐全。
这个我没咋在网上看到使用的方式,好不容易才找到的一个 Demo。文档也没有找到,最后在 npm 上找到该库的一个简单示例。
我拿 Demo 中的实现简单做个说明:
cluster = new Cluster.View(map, {
clusterMinPoints: 2, // 聚合的最小点数
clusterMaxZoom: 18, // 聚合最大放大的比例
updateRealTime: true, // 实时计算更新
fitViewOnClick: true,
clusterType: [ // 根据放大系数进行聚合分类
[3, 10, Cluster.ClusterType.GEO_FENCE, [11001, 11002]],
[11, 11, Cluster.ClusterType.ATTR_REF, 'city'],
[12, 12, Cluster.ClusterType.ATTR_REF, ['city', 'area']],
[13, null, Cluster.ClusterType.DIS_PIXEL, 64],
],
clusterDictionary: (type, key) => { // 聚合字典,将 clusterType 中聚合的点位进行映射转换
if (type === Cluster.ClusterType.GEO_FENCE) {
var properties = REGION[key]
if (properties && properties.center) {
return {
point: properties.center,
region: properties.fence,
}
}
} else if (type === Cluster.ClusterType.ATTR_REF) {
var properties = DISTRICT[key]
if (properties && properties.center) {
return {
point: properties.center,
}
}
}
return null
},
renderClusterStyle: { // 聚合点位渲染
type: Cluster.ClusterRender.DOM,
style: {
anchors: [0, 1],
offsetX: -20,
offsetY: -9.5,
},
inject: getHTMLDOM, // 自定义实现点位DOM
},
renderSingleStyle: { // 普通点位渲染
type: Cluster.ClusterRender.DOM,
style: {
anchors: [0, 1],
offsetX: -20,
offsetY: -9.5,
},
inject: getHTMLDOM, // 自定义实现点位DOM
},
})
着重来看一下后面四个属性,一个个来详细分析一下:
-
clusterType[minZoom, maxZoom, type, other]通过 Demo 其实也可以看出来,在不同的放大系数下,呈现的聚合效果是不一样的。
[3, 11, Cluster.ClusterType.ATTR_REF, 'city']拿这个解释:在放大系数为 3~11 之间的时候,聚合类型为ATTR_REF,也就是点位上面的属性进行判断,后面跟着的就是进行聚合分类的属性city。
同理,[13, null, Cluster.ClusterType.DIS_PIXEL, 64] 就是 13~∞ 放大系数下,根据距离进行聚合。
同时,也可以看到在聚合范围之外的一个 Marker 显示为原本的 Marker 了。
-
getHTMLDOM的话,就是对实际渲染 DOM 的样式操作,下面在实际案例中再具体讲解。 -
pointTransformer就是将数据转换成 Cluster 能识别的数据。
新版地图主题
这里额外说明一下,如何在百度地图中修改其主题,可以看这个文档
JavaScript API - 个性化地图、自定义地图 | 百度地图API SDK
将其主题的 JSON 文件给下载下来。
然后在代码中进行绑定即可:
<Map
// ...
mapStyleV2={{ styleJson: mapStyle }}
/>
实践
接下来,我们把使用 @bmapgl-plugin/cluster 来完成点聚合操作。
因为 @bmapgl-plugin/cluster 的原因,我们再使用组件的方式去添加 Marker 了。
所以只能在初始化的时候去做了。
简单示例:
const Container = () => {
// 获取地图实例,用于点聚合操作
const mapRef = useRef<any>(null)
const clusterRef = useRef<Cluster.View>()
useEffect(() => {
const map = mapRef.current.map
clusterRef.current = new Cluster.View(map, {
clusterMinPoints: 2,
clusterMaxZoom: 18,
updateRealTime: true,
fitViewOnClick: true
})
const points = Cluster.pointTransformer(
markerList,
item => {
return {
point: [item.lng, item.lat],
properties: {
name: item.name,
province: item.province,
provinceCode: item.provinceCode
}
}
}
)
clusterRef.current.setData(points)
}, [])
return (
<div style={{ width, height }}>
<Map
ref={mapRef}
zoom={6}
center={{ lat: 37.41918484360441, lng: 104.12234245932834 }}
style={{ height: '100%' }}
enableDragging
mapType='normal'
enableScrollWheelZoom
mapStyleV2={{ styleJson: mapStyle }}
/>
</div>
)
}
效果如下:
省份划分
接下来,我们来实现按省份进行点聚合。
要实现这种方式,首先,我们需要把 clusterMinPoints 设为 1,因为这时候我们不是通过点之间的位置差来聚合了,而是需要根据放大比例在进行判断了。这里就需要用到之前提到的 clusterType 了。
以一开始提供的 markerList 为例,将他进行省级划分。
clusterType: [
[3, 8, Cluster.ClusterType.ATTR_REF, 'provinceCode'],
[9, null, Cluster.ClusterType.DIS_PIXEL, 0]
]
忘了里面参数含义的可以向上再看一下。
[9, null, Cluster.ClusterType.DIS_PIXEL, 0] 这里是对9以后的放大系数不做聚合处理,这样聚合节点实际上就是一个个 Marker,只是被包装成了 Cluster。
接着,再重写一下 Cluster 的 DOM,显示出对应的省份信息。
利用 renderClusterStyle 重写 DOM 渲染逻辑。
renderClusterStyle: {
type: Cluster.ClusterRender.DOM,
inject: getHTMLDOM
}
getHTMLDOM 会提供一个当前 Cluster 的 context,能获取聚合的一些相关信息。
然后根据提供的信息,返回一个 DOM 即可。
province省份名词在文章结尾提供。
const getHTMLDOM = (context) => {
const text = province[+`${context.belongValue}0000`]
const count = context.pointCount || 1 // 聚合中点的总数
const div = document.createElement('div')
div.innerHTML = `<div style='width: 200px;color: #fff;'>${`${text} ${count}个`}</div>`
return div
}
这里要注意一下,我们需要把之前的 clusterMinPoints 变为 1,因为现在是按照省份划分,一个点也要显示,只有放大到一定系数后,才现在最终点位。
效果如下:
但这里会有一个问题,就是需要将 clusterMinPoints 设为 1 了,但都是聚合节点了,那怎么找到 Cluster 对应的 Marker 呢。
可以利用前面进行分级的 ClusterType 进行判断,是省份的聚合点还是改还原回去。
context.type === Cluster.ClusterType.ATTR_REF
那么剩下了的就是大于9(放大系数)的Cluster,其对应一个 Marker。
这里如果做额外的聚合处理的话,其实也可以,如
[9, null, Cluster.ClusterType.DIS_PIXEL, 64]那么就需要在下面的
getHTMLDOM中,除了对以上两个clusterType做处理的前提下,还需要多判断一个放大系数大于9以后的聚合数量,确定Cluster里面包含几个Marker,再做对应处理。
现在只需要处理一个问题,就是如何获取 Cluster 下对应的 Marker。
Cluster 内部提供了两个方法:
getLeaves(clusterId: number, limitNum?: number): any[];
getSonNodes(clusterId: number): any[];
但是,没有说明文档,经过测试的结果来看,是 父Cluster 获取 子Cluster 的,没办法获取到 Cluster 下的 Marker。
所以,后面就只能通过最暴力的方式,进行经纬度匹配,得出对应的 Marker。
// 利用 context.point 获取聚合点位
const findNearestPoint = (point: [number, number]) => {
let offset = 1
let node = null
const points = cluster.engine.points
while (!node && offset <= 5) {
for (const item of points) {
const info = item.geometry.coordinates
if (
`${point[0]}`.startsWith(`${info[0]}`.slice(0, -offset)) &&
`${point[1]}`.startsWith(`${info[1]}`.slice(0, -offset))
) {
node = item
break
}
}
offset++
}
return node
}
结合上面的,修改 getHTMLDOM:
const getHTMLDOM = context => {
const type = context.type
const text = province[+`${context.belongValue}0000`]
const count = context.pointCount || 1 // 聚合中点的总数
const div = document.createElement('div')
if (type === Cluster.ClusterType.ATTR_REF) {
div.innerHTML = `<div style='width: 200px;color: #fff;'>${`${text} ${count}个`}</div>`
} else {
const node = findNearestPoint(context.point)
div.innerHTML = `<div style='width: 200px;color: #fff;'>${node?.name}</div>`
}
return div
}
效果如下:
对于 DOM 中 Cluster 和 Marker 的样式,不给具体的代码了,可以根据自身的需求去设计,也就是简单的 HTML + CSS 操作。
矫正省份显示位置
还有个属性,我们还没讲:clusterDictionary,用来修改 Cluster 显示位置。
我们需要根据省份进行设置,所以,我们需要获取所有省份经纬度的信息。
我们只需要对 ClusterType.ATTR_REF 做处理,函数会给一个 type和 key(provinceCode)来进行后续判断。
provinceWithCenter省份中心经纬度在文章结尾提供。
clusterDictionary: (type, key) => {
if (type === Cluster.ClusterType.ATTR_REF) {
const center = provinceWithCenter[+`${key}0000`]
if (center) {
return {
point: center,
}
}
}
return null
},
省份数据
省份代码
const province = {
110000: '北京市',
120000: '天津市',
130000: '河北省',
140000: '山西省',
150000: '内蒙古自治区',
210000: '辽宁省',
220000: '吉林省',
230000: '黑龙江省',
310000: '上海市',
320000: '江苏省',
330000: '浙江省',
340000: '安徽省',
350000: '福建省',
360000: '江西省',
370000: '山东省',
410000: '河南省',
420000: '湖北省',
430000: '湖南省',
440000: '广东省',
450000: '广西壮族自治区',
460000: '海南省',
500000: '重庆市',
510000: '四川省',
520000: '贵州省',
530000: '云南省',
540000: '西藏自治区',
610000: '陕西省',
620000: '甘肃省',
630000: '青海省',
640000: '宁夏回族自治区',
650000: '新疆维吾尔自治区',
710000: '台湾省',
810000: '香港特别行政区',
820000: '澳门特别行政区',
}
省份中心经纬度
const provinceWithCenter = {
110000: [
116.405285,
39.904989
],
120000: [
117.190182,
39.125596
],
130000: [
114.502461,
38.045474
],
140000: [
112.549248,
37.857014
],
150000: [
111.670801,
40.818311
],
210000: [
123.429096,
41.796767
],
220000: [
125.3245,
43.886841
],
230000: [
126.642464,
45.756967
],
310000: [
121.472644,
31.231706
],
320000: [
118.767413,
32.041544
],
330000: [
120.153576,
30.287459
],
340000: [
117.283042,
31.86119
],
350000: [
119.306239,
26.075302
],
360000: [
115.892151,
28.676493
],
370000: [
117.000923,
36.675807
],
410000: [
113.665412,
34.757975
],
420000: [
114.298572,
30.584355
],
430000: [
112.982279,
28.19409
],
440000: [
113.280637,
23.125178
],
450000: [
108.320004,
22.82402
],
460000: [
110.33119,
20.031971
],
500000: [
106.504962,
29.533155
],
510000: [
104.065735,
30.659462
],
520000: [
106.713478,
26.578343
],
530000: [
102.712251,
25.040609
],
540000: [
91.132212,
29.660361
],
610000: [
108.948024,
34.263161
],
620000: [
103.823557,
36.058039
],
630000: [
101.778916,
36.623178
],
640000: [
106.278179,
38.46637
],
650000: [
87.617733,
43.792818
],
710000: [
121.509062,
25.044332
],
810000: [
114.173355,
22.320048
],
820000: [
113.54909,
22.198951
]
}
到这里,地图整体的功能就差不多了。
如果像对 Marker 做事件操作的话,可以通过 cluster 提供的方法:
clusterRef.current.on(Cluster.ClusterEvent.CLICK, e => {
// e 里面是当前 Cluster 信息
})
比如点击跳出弹窗,跳转等等操作…
更多在地图上的绘制操作,可以看这个文档