高德地图实现自定义区域下钻

源码:https://github.com/wforguo/amap-drill
演示:https://forguo.cn/app/amap-drill.html

一、地图下钻

  • antv区域钻取

https://l7.antv.vision/zh/examples/choropleth/drill#order-drill
antv-under.gif

  • 高德区域钻取

https://lbs.amap.com/demo/amap-ui/demos/amap-ui-districtexplorer/index
gaode-under.gif

上面是关于正常的省市区的一个地图下钻,现成的地图或者组件可以实现,
但是如果遇到自定义的地图层级钻取就哑火了

二、需求

先来看一下最终效果

gaode-under-custom-min.gif

如图,最终我们要实现科室、部门、经销商的三级钻取,类似我们所说的大区下钻;

方案大概有两个,

  • a、每个区域数据都用svg勾勒出来,每次点击通过id及层级去切换
  • b、通过高德地图尺量图去完成;

显然,a方案耗时,不够友好,下面就用高德来实现这个需求;

三、自定义区域

可以用来实现自定义科室,自定义部门以及自定义经销商

高德尺量图形

https://lbs.amap.com/api/jsapi-v2/documentation#polygon

翻阅高德api,发现AMap.Polygon这个api,可以绘制构造多边形对象,通过PolygonOptions指定多边形样式

这里有path这个参数,就可以把自定义的每一级所对应的地区边缘经纬度坐标拿到,用尺量图渲染出来,就得到了一个自定义的区域,也就可以绘制各种想要的异形地图了

image.png

实现

参考示列:https://lbs.amap.com/demo/jsapi-v2/example/overlayers/polygon-draw

1、先拿到当前自定义区域所对应的经纬度坐标数组,这里以上海为列子

核心代码:

  1. /**
  2. * 需要绘制的经纬度数据源
  3. * 三维数组,这里以上海为列子
  4. */
  5. let paths = [
  6. // 由于每个区域并非是连一起的,所以每个小的区域是去绘制的,
  7. [
  8. // 这里的经纬度是一个数组,由于参数 path 是这种格式,保持一致即可
  9. [121.7789, 31.3102],
  10. [121.5723, 31.4361],
  11. [121.5624, 31.4864],
  12. [121.7694, 31.3907],
  13. [121.7789, 31.3102],
  14. ],
  15. [
  16. [121.9433, 31.2155],
  17. [121.9573, 31.2304],
  18. [122.0086, 31.221],
  19. [121.9957, 31.1608],
  20. [121.9596, 31.1593],
  21. [121.9433, 31.2155],
  22. ],
  23. ];

2、遍历该区域下的坐标,绘制每个子区域矢量图

小细节:每个小区域都需要用尺量图绘制,一起绘制是可以的,但是后面地图的自适应就不好使了

核心代码:

  1. /**
  2. * 尺量图集合
  3. */
  4. let polygons = [];
  5. /**
  6. * 构造多边形对象
  7. * @param path 多边形轮廓线的节点坐标数组
  8. * @param color
  9. */
  10. let addPolygon = function (path, color) {
  11. // 用于在地图上绘制线、面等矢量地图要素的类型
  12. let polygon = new AMap.Polygon({
  13. strokeWeight: 2, // 线条宽度,默认为 1
  14. path: path, // 多边形轮廓线的节点坐标数组
  15. fillOpacity: 0.4,
  16. clickable: false,
  17. fillColor: color, // 多边形填充颜色
  18. strokeColor: color, // 线条颜色
  19. lineJoin: 'round', // 折线拐点的绘制样式,默认值为'miter'尖角,其他可选值:'round'圆角、'bevel'斜角
  20. });
  21. polygons.push(polygon);
  22. }
  23. /**
  24. * tips:小细节,
  25. * 每个小区域都需要用尺量图绘制,一起绘制是可以的,但是后面地图的自适应就不好使了
  26. * 遍历每个小区域并绘制
  27. * @param path 多边形轮廓线的节点坐标数组
  28. */
  29. paths.map(path => {
  30. addPolygon(path);
  31. });

3、添加到地图,并做自适应

小细节:渲染到地图之后,用setFitView做个地图窗口自适应

  1. // 渲染尺量图到地图
  2. map.add(polygons);
  3. /**
  4. * tips:小细节,
  5. * 绘制完成之后,做个窗口自适应
  6. */
  7. map.setFitView(polygons);

4、添加中心点Marker

https://lbs.amap.com/api/jsapi-v2/documentation#marker

自定义区域绘制好了,接下来就得将区域名称及数据展示出来
需要使用Marker组件将对应信息绘制在这个区域的中心位置

这里的Marker有两个用途

  • 展示区域信息及相关数据
  • 通过点击实现地图下钻

通过MarkerextData属性来携带当前层级数据,便于下一级的钻取

image.png

核心代码:

  1. /**
  2. * 自定义marker内容
  3. * @param item { title: '', count: '', position: []}
  4. * @returns {string}
  5. */
  6. let renderMarker = function (item) {
  7. const {
  8. title = '',
  9. count = 0,
  10. center = [],
  11. } = item;
  12. // 创建纯文本标记
  13. let marker = new AMap.Marker({
  14. content: `<div class='area-map-marker' style='color: ${item.color || '#000'}'>
  15. <div class='area-map-marker__title' style='font-weight: bold;'>${title}</div>
  16. <div class='area-map-marker__title'>${count || 0}</div>
  17. </div>`,
  18. anchor: 'center', // 设置文本标记锚点
  19. draggable: false,
  20. cursor: 'pointer',
  21. position: center,
  22. extData: item,
  23. zIndex: 1000,
  24. });
  25. markers.push(marker);
  26. // 通过点击实现地图下钻
  27. marker.on('mousedown', (e) => {
  28. handleAreaClick(e);
  29. });
  30. marker.setMap(map);
  31. }

这里需要将每个merker放到一个集合markers,用于后期的回收

  1. // 清空markers
  2. if (markers.length > 0) {
  3. map.remove(markers);
  4. markers = [];
  5. }

5、中心位置的获取

通过当前所有的经纬度集合【原数据需要做展开处理】,计算得到中心点的经纬度

核心代码:

  1. /**
  2. * 获取随机数
  3. */
  4. function getRandomNum (min, max) {
  5. return Math.floor(Math.random() * (max - min)) + min;
  6. }
  7. /**
  8. * @desc 返回中心点的[经度,纬度]
  9. * @param points points = [[经度,纬度], [经度,纬度]]; 参数数组points的每一项为每一个点的:[经度,纬度]
  10. * @returns {number[]} 返回中心点的数组[经度,纬度]
  11. */
  12. function getPointsCenter (points) {
  13. try {
  14. let point_num = points.length; // 坐标点个数
  15. let X = 0, Y = 0, Z = 0;
  16. for (let i = 0; i < points.length; i++) {
  17. if (points[i] == '') {
  18. continue;
  19. }
  20. let point = points[i];
  21. let lat, lng, x, y, z;
  22. lng = parseFloat(point[0]) * Math.PI / 180;
  23. lat = parseFloat(point[1]) * Math.PI / 180;
  24. x = Math.cos(lat) * Math.cos(lng);
  25. y = Math.cos(lat) * Math.sin(lng);
  26. z = Math.sin(lat);
  27. X += x;
  28. Y += y;
  29. Z += z;
  30. }
  31. X = X / point_num;
  32. Y = Y / point_num;
  33. Z = Z / point_num;
  34. let tmp_lng = Math.atan2(Y, X);
  35. let tmp_lat = Math.atan2(Z, Math.sqrt(X * X + Y * Y));
  36. // 经纬度分别小数点后2位加随机数,防止Marker完全重叠
  37. let x = getRandomNum(2, 12) * 0.01;
  38. let y = getRandomNum(3, 12) * 0.01;
  39. return [(tmp_lng * 180 / Math.PI) + x, (tmp_lat * 180 / Math.PI) + y];
  40. } catch (e) {
  41. console.warn('获取中心坐标失败');
  42. console.log(e);
  43. }
  44. }

四、数据整合

上面的是一个简单步骤,最重要的还是数据,这里需要得到两个数据

经纬度边缘坐标

https://lbs.amap.com/api/webservice/guide/api/district/
通过省市区code看来查询全国所有的省、市、区对应的经纬度边缘坐标,并通过省市区code关联

extensionsall,才能得到对应的边界坐标,这个也最好让服务端来批量获取并存下来,
数据比较多,可以做稀疏处理,大概6倍即可,当然数据越多轮廓越精细

  1. https://restapi.amap.com/v3/config/district?keywords=310000&key=56e119b97e84efd95dbca95cd2be3126&subdistrict=2&extensions=all

polyline就是我们最终需要的经纬度边缘坐标集合了,然后整合成二位数组
image.png

将省市区code和经纬度数组整合成Object,键为省市区code,值为坐标集合

最终结构如下
最好可以放在CDN,来做一个缓存

  1. // 对应上海,苏州和无锡
  2. let areaPath = {
  3. "310000": [
  4. // 由于每个区域并非是连一起的,所以每个小的区域是去绘制的,
  5. [
  6. // 这里的经纬度是一个数组,由于参数 path 是这种格式,保持一致即可
  7. [121.7789, 31.3102],
  8. [121.5723, 31.4361],
  9. [121.5624, 31.4864],
  10. [121.7694, 31.3907],
  11. [121.7789, 31.3102],
  12. ],
  13. [
  14. [121.627, 31.445],
  15. [121.5758, 31.4782],
  16. [121.635, 31.453],
  17. [121.627, 31.445],
  18. ],
  19. [
  20. [121.9433, 31.2155],
  21. [121.9573, 31.2304],
  22. [122.0086, 31.221],
  23. [121.9957, 31.1608],
  24. [121.9596, 31.1593],
  25. [121.9433, 31.2155],
  26. ],
  27. ],
  28. "320500": [
  29. [[120.57023,
  30. 31.66932], [120.56821, 31.68546], [120.58645, 31.69071], [120.60081, 31.70885], [120.58245, 31.72117], [
  31. 120.58436, 31.73447], [120.60002, 31.74463], [120.58424, 31.78215], [120.57071, 31.79378], [120.55838,
  32. 31.78571], [120.55589, 31.7942], [120.53156, 31.78779], [120.52254, 31.80629], [120.53131, 31.82785], [
  33. 120.50328, 31.84171], [120.49088, 31.87133], [120.46882, 31.87962], [120.4665, 31.88998], [120.37867,
  34. 31.91374], [120.39126, 31.92861], [120.37353, 31.94644], [120.3707, 31.99082], [120.40376, 32.01622], [
  35. 120.46567, 32.04583], [120.5038, 32.04102], [120.62839, 32.00117], [120.76158, 32.02045], [120.78204,
  36. 32.01599], [120.80313, 31.98844], [120.86033, 31.87306], [120.91664, 31.79366], [120.9595, 31.78304], [
  37. 121.06064, 31.78306], [121.10122, 31.76252], [121.14533, 31.75392], [121.28911, 31.61628], [121.37221,
  38. 31.55321], [121.3435, 31.51206]
  39. ]
  40. ],
  41. "320200": [
  42. [[120.3707, 31.99082], [120.37353, 31.94644], [120.39126, 31.92861], [120.37867, 31.91374], [120.4665,
  43. 31.88998], [120.46882, 31.87962], [120.49088, 31.87133], [120.50328, 31.84171], [120.53131, 31.82785], [
  44. 120.52254, 31.80629], [120.53156, 31.78779], [120.55589, 31.7942], [120.55838, 31.78571], [120.57071,
  45. 31.79378], [120.59766, 31.75503], [120.60002, 31.74463], [120.58171, 31.72763], [120.58509, 31.71443]]
  46. ],
  47. }

每一级部门数据及对应的区域code集合

下钻的每一级区域及对应数据是已知的,这里的数据已经存有区域code,
所以就可以很好的和经纬度数据做一个关联

区域数据结构如下
这个数据一般由接口返回

  1. let areaList = [
  2. {
  3. "id": "2",
  4. "name": "华东科",
  5. "level": 2,
  6. "levelTitle": "科室",
  7. "count": 100,
  8. "areaIdList": [
  9. "310000",
  10. "320000",
  11. "330000",
  12. "370000",
  13. "420000",
  14. "500000"
  15. ]
  16. },
  17. {
  18. "id": "3",
  19. "name": "华南科",
  20. "mapTier": 2,
  21. "levelTitle": "科室",
  22. "count": 100,
  23. "areaIdList": [
  24. "350000",
  25. "360000",
  26. "430000",
  27. "450000",
  28. "440000",
  29. "420000",
  30. "460000",
  31. "520000",
  32. "530000"
  33. ]
  34. },
  35. {
  36. "id": "4",
  37. "name": "西北科",
  38. "level": 2,
  39. "levelTitle": "科室",
  40. "count": 100,
  41. "areaIdList": [
  42. "610000",
  43. "650000",
  44. "500000",
  45. "620000",
  46. ]
  47. },
  48. {
  49. "id": "5",
  50. "name": "华北科",
  51. "level": 2,
  52. "levelTitle": "科室",
  53. "count": 100,
  54. "areaIdList": [
  55. "120000",
  56. "130000",
  57. "140000",
  58. "370000",
  59. "340000"
  60. ]
  61. },
  62. {
  63. "id": "7",
  64. "name": "东北科",
  65. "level": 2,
  66. "levelTitle": "科室",
  67. "count": 100,
  68. "areaIdList": [
  69. "110000",
  70. "150000",
  71. "230000",
  72. "220000",
  73. "210000"
  74. ]
  75. }
  76. ]

这里的id和level根据业务需要来定,
当前level是按照如下划分:
1:国家
2:科室
3:部门
4:经销商
5:区县

五、地图下钻

下钻其实就是获取到下一级的区域数据,并渲染到地图

在marker渲染的时候,添加了事件的处理,

  1. // 通过点击实现地图下钻
  2. marker.on('mousedown', (e) => {
  3. handleAreaClick(e);
  4. });

就可以在事件回调中来根据当前层级来获取下一层级的数据,并完成地图的渲染

  1. let handleAreaClick = function () {
  2. const data = e.target.De.extData;
  3. const {
  4. level,
  5. levelTitle,
  6. id,
  7. count,
  8. } = data;
  9. getMapData(id, level);
  10. }
  11. // 默认从科室层级开始
  12. let getMapData = function (id, level = 2) {
  13. // 接口获取
  14. getUnderData({
  15. id,
  16. level: level + 1,
  17. }).then(res => {
  18. console.log(res);
  19. let areaList = res;
  20. areaList.map(item => {
  21. let position = [];
  22. item.areaList.map(areaId => {
  23. let paths = areaPath[areaId];
  24. paths.map(path => {
  25. position = [...position, ...path];
  26. addPolygon(path);
  27. });
  28. });
  29. // 获取中心点坐标,并渲染区域名称及数据marker
  30. let center = getPointsCenter(position);
  31. renderMarker({
  32. ...item,
  33. center,
  34. color: '#ccc',
  35. });
  36. });
  37. })
  38. }

至此,下钻功能完成,源码请移步 https://github.com/wforguo/amap-drill