Skip to content

利用高德api实现自定义区域下钻

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

源码: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/indexgaode-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、先拿到当前自定义区域所对应的经纬度坐标数组,这里以上海为列子

核心代码:

javascript
  /**
   * 需要绘制的经纬度数据源
   * 三维数组,这里以上海为列子
   */
  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、遍历该区域下的坐标,绘制每个子区域矢量图

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

核心代码:

javascript
/**
 * 尺量图集合
 */
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做个地图窗口自适应

javascript
// 渲染尺量图到地图
map.add(polygons);

/**
 * tips:小细节,
 * 绘制完成之后,做个窗口自适应
 */
map.setFitView(polygons);

4、添加中心点Marker

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

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

这里的Marker有两个用途

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

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

image.png

核心代码:

javascript
  /**
   * 自定义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,用于后期的回收

javascript
// 清空markers
if (markers.length >  0) {
    map.remove(markers);
    markers = [];
}

5、中心位置的获取

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

核心代码:

javascript
/**
 * 获取随机数
 */
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关联

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

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

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

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

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

javascript
// 对应上海,苏州和无锡
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,
所以就可以很好的和经纬度数据做一个关联

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

javascript
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渲染的时候,添加了事件的处理,

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

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

javascript
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

...