高德地图实现自定义区域下钻
源码: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, // 线条宽度,默认为 1path: 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,用于后期的回收
// 清空markersif (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);});});// 获取中心点坐标,并渲染区域名称及数据markerlet center = getPointsCenter(position);renderMarker({...item,center,color: '#ccc',});});})}
至此,下钻功能完成,源码请移步 https://github.com/wforguo/amap-drill
…
