高德地图实现自定义区域下钻
源码:https://github.com/wforguo/amap-drill
演示:https://forguo.cn/app/amap-drill.html
一、地图下钻
- antv区域钻取
https://l7.antv.vision/zh/examples/choropleth/drill#order-drill
- 高德区域钻取
https://lbs.amap.com/demo/amap-ui/demos/amap-ui-districtexplorer/index
上面是关于正常的省市区的一个地图下钻,现成的地图或者组件可以实现,
但是如果遇到自定义的地图层级钻取就哑火了
二、需求
先来看一下最终效果
如图,最终我们要实现科室、部门、经销商的三级钻取,类似我们所说的大区下钻;
方案大概有两个,
- a、每个区域数据都用svg勾勒出来,每次点击通过id及层级去切换
- b、通过高德地图尺量图去完成;
显然,a方案耗时,不够友好,下面就用高德来实现这个需求;
三、自定义区域
可以用来实现自定义科室,自定义部门以及自定义经销商
高德尺量图形
https://lbs.amap.com/api/jsapi-v2/documentation#polygon
翻阅高德api,发现AMap.Polygon
这个api,可以绘制构造多边形对象,通过PolygonOptions
指定多边形样式
这里有path
这个参数,就可以把自定义的每一级所对应的地区边缘经纬度坐标拿到,用尺量图渲染出来,就得到了一个自定义的区域,也就可以绘制各种想要的异形地图了
实现
参考示列:https://lbs.amap.com/demo/jsapi-v2/example/overlayers/polygon-draw
1、先拿到当前自定义区域所对应的经纬度坐标数组,这里以上海为列子
核心代码:
/**
* 需要绘制的经纬度数据源
* 三维数组,这里以上海为列子
*/
let paths = [
// 由于每个区域并非是连一起的,所以每个小的区域是去绘制的,
[
// 这里的经纬度是一个数组,由于参数 path 是这种格式,保持一致即可
[121.7789, 31.3102],
[121.5723, 31.4361],
[121.5624, 31.4864],
[121.7694, 31.3907],
[121.7789, 31.3102],
],
[
[121.9433, 31.2155],
[121.9573, 31.2304],
[122.0086, 31.221],
[121.9957, 31.1608],
[121.9596, 31.1593],
[121.9433, 31.2155],
],
];
2、遍历该区域下的坐标,绘制每个子区域矢量图
小细节:每个小区域都需要用尺量图绘制,一起绘制是可以的,但是后面地图的自适应就不好使了
核心代码:
/**
* 尺量图集合
*/
let polygons = [];
/**
* 构造多边形对象
* @param path 多边形轮廓线的节点坐标数组
* @param color
*/
let addPolygon = function (path, color) {
// 用于在地图上绘制线、面等矢量地图要素的类型
let polygon = new AMap.Polygon({
strokeWeight: 2, // 线条宽度,默认为 1
path: path, // 多边形轮廓线的节点坐标数组
fillOpacity: 0.4,
clickable: false,
fillColor: color, // 多边形填充颜色
strokeColor: color, // 线条颜色
lineJoin: 'round', // 折线拐点的绘制样式,默认值为'miter'尖角,其他可选值:'round'圆角、'bevel'斜角
});
polygons.push(polygon);
}
/**
* tips:小细节,
* 每个小区域都需要用尺量图绘制,一起绘制是可以的,但是后面地图的自适应就不好使了
* 遍历每个小区域并绘制
* @param path 多边形轮廓线的节点坐标数组
*/
paths.map(path => {
addPolygon(path);
});
3、添加到地图,并做自适应
小细节:渲染到地图之后,用
setFitView
做个地图窗口自适应
// 渲染尺量图到地图
map.add(polygons);
/**
* tips:小细节,
* 绘制完成之后,做个窗口自适应
*/
map.setFitView(polygons);
4、添加中心点Marker
https://lbs.amap.com/api/jsapi-v2/documentation#marker
自定义区域绘制好了,接下来就得将区域名称及数据展示出来
需要使用Marker
组件将对应信息绘制在这个区域的中心位置
这里的Marker
有两个用途
- 展示区域信息及相关数据
- 通过点击实现地图下钻
通过Marker
的extData
属性来携带当前层级数据,便于下一级的钻取
核心代码:
/**
* 自定义marker内容
* @param item { title: '', count: '', position: []}
* @returns {string}
*/
let renderMarker = function (item) {
const {
title = '',
count = 0,
center = [],
} = item;
// 创建纯文本标记
let marker = new AMap.Marker({
content: `<div class='area-map-marker' style='color: ${item.color || '#000'}'>
<div class='area-map-marker__title' style='font-weight: bold;'>${title}</div>
<div class='area-map-marker__title'>${count || 0}</div>
</div>`,
anchor: 'center', // 设置文本标记锚点
draggable: false,
cursor: 'pointer',
position: center,
extData: item,
zIndex: 1000,
});
markers.push(marker);
// 通过点击实现地图下钻
marker.on('mousedown', (e) => {
handleAreaClick(e);
});
marker.setMap(map);
}
这里需要将每个merker
放到一个集合markers
,用于后期的回收
// 清空markers
if (markers.length > 0) {
map.remove(markers);
markers = [];
}
5、中心位置的获取
通过当前所有的经纬度集合【原数据需要做展开处理】,计算得到中心点的经纬度
核心代码:
/**
* 获取随机数
*/
function getRandomNum (min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
/**
* @desc 返回中心点的[经度,纬度]
* @param points points = [[经度,纬度], [经度,纬度]]; 参数数组points的每一项为每一个点的:[经度,纬度]
* @returns {number[]} 返回中心点的数组[经度,纬度]
*/
function getPointsCenter (points) {
try {
let point_num = points.length; // 坐标点个数
let X = 0, Y = 0, Z = 0;
for (let i = 0; i < points.length; i++) {
if (points[i] == '') {
continue;
}
let point = points[i];
let lat, lng, x, y, z;
lng = parseFloat(point[0]) * Math.PI / 180;
lat = parseFloat(point[1]) * Math.PI / 180;
x = Math.cos(lat) * Math.cos(lng);
y = Math.cos(lat) * Math.sin(lng);
z = Math.sin(lat);
X += x;
Y += y;
Z += z;
}
X = X / point_num;
Y = Y / point_num;
Z = Z / point_num;
let tmp_lng = Math.atan2(Y, X);
let tmp_lat = Math.atan2(Z, Math.sqrt(X * X + Y * Y));
// 经纬度分别小数点后2位加随机数,防止Marker完全重叠
let x = getRandomNum(2, 12) * 0.01;
let y = getRandomNum(3, 12) * 0.01;
return [(tmp_lng * 180 / Math.PI) + x, (tmp_lat * 180 / Math.PI) + y];
} catch (e) {
console.warn('获取中心坐标失败');
console.log(e);
}
}
四、数据整合
上面的是一个简单步骤,最重要的还是数据,这里需要得到两个数据
经纬度边缘坐标
https://lbs.amap.com/api/webservice/guide/api/district/
通过省市区code看来查询全国所有的省、市、区对应的经纬度边缘坐标,并通过省市区code关联
extensions
为all
,才能得到对应的边界坐标,这个也最好让服务端来批量获取并存下来,
数据比较多,可以做稀疏处理,大概6倍即可,当然数据越多轮廓越精细
https://restapi.amap.com/v3/config/district?keywords=310000&key=56e119b97e84efd95dbca95cd2be3126&subdistrict=2&extensions=all
polyline
就是我们最终需要的经纬度边缘坐标集合了,然后整合成二位数组
将省市区code和经纬度数组整合成Object,键为省市区code,值为坐标集合
最终结构如下
最好可以放在CDN,来做一个缓存
// 对应上海,苏州和无锡
let areaPath = {
"310000": [
// 由于每个区域并非是连一起的,所以每个小的区域是去绘制的,
[
// 这里的经纬度是一个数组,由于参数 path 是这种格式,保持一致即可
[121.7789, 31.3102],
[121.5723, 31.4361],
[121.5624, 31.4864],
[121.7694, 31.3907],
[121.7789, 31.3102],
],
[
[121.627, 31.445],
[121.5758, 31.4782],
[121.635, 31.453],
[121.627, 31.445],
],
[
[121.9433, 31.2155],
[121.9573, 31.2304],
[122.0086, 31.221],
[121.9957, 31.1608],
[121.9596, 31.1593],
[121.9433, 31.2155],
],
],
"320500": [
[[120.57023,
31.66932], [120.56821, 31.68546], [120.58645, 31.69071], [120.60081, 31.70885], [120.58245, 31.72117], [
120.58436, 31.73447], [120.60002, 31.74463], [120.58424, 31.78215], [120.57071, 31.79378], [120.55838,
31.78571], [120.55589, 31.7942], [120.53156, 31.78779], [120.52254, 31.80629], [120.53131, 31.82785], [
120.50328, 31.84171], [120.49088, 31.87133], [120.46882, 31.87962], [120.4665, 31.88998], [120.37867,
31.91374], [120.39126, 31.92861], [120.37353, 31.94644], [120.3707, 31.99082], [120.40376, 32.01622], [
120.46567, 32.04583], [120.5038, 32.04102], [120.62839, 32.00117], [120.76158, 32.02045], [120.78204,
32.01599], [120.80313, 31.98844], [120.86033, 31.87306], [120.91664, 31.79366], [120.9595, 31.78304], [
121.06064, 31.78306], [121.10122, 31.76252], [121.14533, 31.75392], [121.28911, 31.61628], [121.37221,
31.55321], [121.3435, 31.51206]
]
],
"320200": [
[[120.3707, 31.99082], [120.37353, 31.94644], [120.39126, 31.92861], [120.37867, 31.91374], [120.4665,
31.88998], [120.46882, 31.87962], [120.49088, 31.87133], [120.50328, 31.84171], [120.53131, 31.82785], [
120.52254, 31.80629], [120.53156, 31.78779], [120.55589, 31.7942], [120.55838, 31.78571], [120.57071,
31.79378], [120.59766, 31.75503], [120.60002, 31.74463], [120.58171, 31.72763], [120.58509, 31.71443]]
],
}
每一级部门数据及对应的区域code集合
下钻的每一级区域及对应数据是已知的,这里的数据已经存有区域code,
所以就可以很好的和经纬度数据做一个关联
区域数据结构如下
这个数据一般由接口返回
let areaList = [
{
"id": "2",
"name": "华东科",
"level": 2,
"levelTitle": "科室",
"count": 100,
"areaIdList": [
"310000",
"320000",
"330000",
"370000",
"420000",
"500000"
]
},
{
"id": "3",
"name": "华南科",
"mapTier": 2,
"levelTitle": "科室",
"count": 100,
"areaIdList": [
"350000",
"360000",
"430000",
"450000",
"440000",
"420000",
"460000",
"520000",
"530000"
]
},
{
"id": "4",
"name": "西北科",
"level": 2,
"levelTitle": "科室",
"count": 100,
"areaIdList": [
"610000",
"650000",
"500000",
"620000",
]
},
{
"id": "5",
"name": "华北科",
"level": 2,
"levelTitle": "科室",
"count": 100,
"areaIdList": [
"120000",
"130000",
"140000",
"370000",
"340000"
]
},
{
"id": "7",
"name": "东北科",
"level": 2,
"levelTitle": "科室",
"count": 100,
"areaIdList": [
"110000",
"150000",
"230000",
"220000",
"210000"
]
}
]
这里的id和level根据业务需要来定,
当前level
是按照如下划分:
1:国家
2:科室
3:部门
4:经销商
5:区县
五、地图下钻
下钻其实就是获取到下一级的区域数据,并渲染到地图
在marker渲染的时候,添加了事件的处理,
// 通过点击实现地图下钻
marker.on('mousedown', (e) => {
handleAreaClick(e);
});
就可以在事件回调中来根据当前层级来获取下一层级的数据,并完成地图的渲染
let handleAreaClick = function () {
const data = e.target.De.extData;
const {
level,
levelTitle,
id,
count,
} = data;
getMapData(id, level);
}
// 默认从科室层级开始
let getMapData = function (id, level = 2) {
// 接口获取
getUnderData({
id,
level: level + 1,
}).then(res => {
console.log(res);
let areaList = res;
areaList.map(item => {
let position = [];
item.areaList.map(areaId => {
let paths = areaPath[areaId];
paths.map(path => {
position = [...position, ...path];
addPolygon(path);
});
});
// 获取中心点坐标,并渲染区域名称及数据marker
let center = getPointsCenter(position);
renderMarker({
...item,
center,
color: '#ccc',
});
});
})
}
至此,下钻功能完成,源码请移步 https://github.com/wforguo/amap-drill
…