百度地图的使用(Mark、Cluster…)

1,735 阅读9分钟

对于百度地图的基本使用的话,可以看一下其官方文档, 本文着重讲解关于 Marker 标注的使用以及点聚合并结合到地图各省的聚合实现 。

地图 JS API | 百度地图API SDK

这里会使用到 react-bmapgl 这个库,其是百度地图的 React 封装库,详细使用可以参考其官方文档 。

React-BMapGL文档

使用方式和 API 基本和原生一致,只是将控件转换成了组件以及 API 转换成了属性,内部最终还是调用原生的 API 实现。

最终实现效果展示:

20241011173317_rec_-convert.gif

前置

先从简单的开始,先看看最简单的 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>
  )
}

效果如下:

image (3).png

Map 组件上的 API 属性可以参考以下链接的类参考:

百度地图JSAPI 3.0类参考

点聚合方案

接下来,我们来看看点聚合。

百度地图本身是不支持点聚合功能的,这里的实现来自扩展内容;我找到了两种实现方式,可以分为两类,简单使用和复杂使用。

MarkerClusterer(简单使用)

官方 Demo:

地图JS API示例 | 百度地图开放平台

类参考:

BMapLibrary - BMapLib.MarkerClusterer

这种方式,只是简单的实现点聚合,或者是解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能。

这个在网上有很多实现,并利用它去修改源码实现的一些参考。

但是,这种方式的可扩展性很大,它只有两个源文件,完全可以下下来自行修改其内容,根据自己的需求去调整,但整体来说,过于复杂,只适合简单使用。

@bmapgl-plugin/cluster(复杂使用)

官方 Demo:

地图JS API示例 | 百度地图开放平台

这种方式的配置相对来说比较复杂,但功能齐全。

这个我没咋在网上看到使用的方式,好不容易才找到的一个 Demo。文档也没有找到,最后在 npm 上找到该库的一个简单示例。

npm: @bmapgl-plugin/cluster

我拿 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

image (4).png

同理,[13, null, Cluster.ClusterType.DIS_PIXEL, 64] 就是 13~∞ 放大系数下,根据距离进行聚合。

image (5).png

同时,也可以看到在聚合范围之外的一个 Marker 显示为原本的 Marker 了。

  • getHTMLDOM 的话,就是对实际渲染 DOM 的样式操作,下面在实际案例中再具体讲解。

  • pointTransformer 就是将数据转换成 Cluster 能识别的数据。

新版地图主题

这里额外说明一下,如何在百度地图中修改其主题,可以看这个文档

 JavaScript API - 个性化地图、自定义地图 | 百度地图API SDK

将其主题的 JSON 文件给下载下来。

image (6).png

然后在代码中进行绑定即可:

<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>
  )
}

效果如下:

image (7).png

省份划分

接下来,我们来实现按省份进行点聚合。

要实现这种方式,首先,我们需要把 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,因为现在是按照省份划分,一个点也要显示,只有放大到一定系数后,才现在最终点位。

效果如下:

image (8).png

但这里会有一个问题,就是需要将 clusterMinPoints 设为 1 了,但都是聚合节点了,那怎么找到 Cluster 对应的 Marker 呢。

image (9).png

可以利用前面进行分级的 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
}

效果如下:

image (10).png

对于 DOM 中 ClusterMarker 的样式,不给具体的代码了,可以根据自身的需求去设计,也就是简单的 HTML + CSS 操作。

矫正省份显示位置

还有个属性,我们还没讲:clusterDictionary,用来修改 Cluster 显示位置。

我们需要根据省份进行设置,所以,我们需要获取所有省份经纬度的信息。

我们只需要对 ClusterType.ATTR_REF 做处理,函数会给一个 typekeyprovinceCode)来进行后续判断。

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 信息
})

比如点击跳出弹窗,跳转等等操作…

更多在地图上的绘制操作,可以看这个文档

 JavaScript API - 标注 | 百度地图API SDK