分享一下地图上的标记点按省市区聚合的实现方法
高德地图
直接按官方示例做就好了,非常方便
谷歌地图
谷歌地图官方示例只有按距离聚合,没有按索引聚合的例子,需要自己实现
这里要用到MarkerManager,源文件不大,可以直接copy到项目里
准备数据
yaml
this.clusterFields = ['province', 'city', 'district']
this.points = [
{
serial: 1,
label: xxx,
address: xxx,
position: { longitude: xxx, latitude: xxx },
province: xx省,
city: xx市,
district: xx区
},
...
]
初始化地图
php
this.map = new google.maps.Map(
document.getElementById("map") as HTMLElement,
{
zoom: 3,
center: { lat: -28.024, lng: 140.887 },
mapId: 'DEMO_MAP_ID',
}
);
初始化MarkerManager
kotlin
this.mgr = new MarkerManager(this.map, {})
google.maps.event.addListener(this.mgr, 'loaded', () => {
// 需要在loaded之后才能用
this.generateMarkers()
})
聚合规则
ini
const generateMarkers = () => {
// 重新渲染点时清空一下
this.mgr.clearMarkers()
// 按省市区分类好标记点
const levelObj = {}
const allMarkers = this.points.map((p) => {
for (const field of this.clusterFields) {
if (!levelObj[field]) {
levelObj[field] = {}
}
// 每个省市区只取第一个数据作为中心点
if (p[field]) {
if (!levelObj[field][p[field]]) {
levelObj[field][p[field]] = { data: p, count: 1 }
} else {
levelObj[field][p[field]].count++
}
}
}
// 不聚合时显示的标记点
const marker = new google.maps.Marker({
position: { lng: p.position.longitude, lat: p.position.latitude },
optimized: true,
label: p.label,
zIndex: p.serial
})
// 点击标记显示卡片信息
marker.addListener('click', () => {
if (!this.infoWindow || !this.infoWindow.open) {
const position = marker.getPosition()
// infoWindow要依存有map的marker才能显示。当前marker的map会随着缩放变为null,所以copy一个不可见的marker
let copyMarker = new google.maps.Marker({
map: this.map,
position: position,
optimized: true
visible: false
})
this.infoWindow = new google.maps.InfoWindow({
content: p.address,
disableAutoPan: true
})
this.infoWindow.addListener('close', () => {
// infoWindow关闭时移除copyMarker
copyMarker.setMap(null)
copyMarker = null
})
this.infoWindow.open({ anchor: copyMarker, shouldFocus: false })
}
})
return marker
})
// 聚合时显示的标记
this.clusterFields.forEach((field, index) => {
// 转化为省市区数组
levelObj[field] = Object.values(levelObj[field]).map((obj) => {
const p = obj.data
const count = obj.count
let size = 70 + index * 10
const label = p[field]
const div = document.createElement('div')
div.className = 'map-cluster'
div.style.backgroundColor = 'rgba(255, 57, 57, 0.8)'
div.style.backgroundSize = 'cover'
div.style.width = size + 'px'
div.style.height = size + 'px'
div.style.borderRadius = size + 'px'
div.innerHTML = `<span class="showName" title="${label}">${label}</span><span class="showCount">${count}</span>`
div.style.color = '#ffffff'
div.style.textAlign = 'center'
div.onclick = () => {
let curZoom = this.map.getZoom()
if (curZoom < 20) {
curZoom += 1
}
this.map.setZoom(curZoom)
this.map.setCenter({ lng: p.position.longitude, lat: p.position.latitude })
}
const marker = new google.maps.Marker({
position: { lng: p.position.longitude, lat: p.position.latitude },
optimized: true,
visible: false // 不显示marker
})
// 谷歌的marker无法显示两行文字,这里使用InfoWindow挂在marker上,并隐藏marker
const infowindow = new google.maps.InfoWindow({
content: div,
disableAutoPan: true,
pixelOffset: new google.maps.Size(0, size / 2 + 6)
})
infowindow.open({ anchor: marker, shouldFocus: false })
})
})
// 序号级别显示全部标记
levelObj['serial'] = allMarkers
// 平均分配层级
const totalLevels = this.clusterFields.length
const zoomStart = 11
const zoomEnd = 1
const zoomStep = (zoomStart - zoomEnd) / totalLevels
// 第一步:先计算所有层级的 maxZoom
const maxZooms = []
for (let i = 0; i < totalLevels; i++) {
const maxZoom = Math.floor(zoomStart - i * zoomStep)
maxZooms.push(maxZoom)
}
// 第二步:根据 maxZoom 推导 minZoom
for (let i = 0; i < totalLevels; i++) {
let minZoom
if (i < totalLevels - 1) {
minZoom = maxZooms[i + 1] + 1
} else {
minZoom = 1 // 最底层的 minZoom 设为 1
}
this.mgr.addMarkers(levelObj[this.clusterFields[i]], Math.max(minZoom, 1), maxZooms[i])
}
this.mgr.addMarkers(levelObj['serial'], 12, 20)
// 将MarkerManager添加到地图上
this.mgr.refresh()
}
效果:

至此,主要代码就结束了。
如果数据变更了,需要重新调用generateMarkers方法。该方法较为庞大复杂,性能不高,我也没找到其他好办法,如果有更好的方案,可以探讨下。