• Echarts热力/散点/面积地图和高德amap不得不说的故事


    简单封装高德amap

            只要涉及到地图开发,我们都需要依赖地图工具,常见的有谷歌地图、百度地图、高德地图。我们的项目里依赖高德地图JS API 2.0。

    npm i @amap/amap-jsapi-loader -s 

          在项目里,我们需要一个预加载好的地图loader方便我们随调随用。这里简单封装一下。

    1. // amap.js
    2. import AMapLoader from '@amap/amap-jsapi-loader';
    3. import { promiseLock } from '@triascloud/utils';
    4. /**
    5. * 高德地图初始化工具
    6. */
    7. class AMapHelper {
    8. static getAMap = window.AMap
    9. ? window.AMap
    10. : promiseLock(AMapHelper.setLoader, {
    11. keyGenerator: () => 'AMapHelper',
    12. forever: true,
    13. global: true,
    14. });
    15. static async setLoader() {
    16. return await AMapLoader.load({
    17. key: process.env.VUE_APP_AMAP_API_KEY,
    18. version: '2.0',
    19. plugins: [
    20. 'AMap.Geocoder',
    21. 'AMap.Geolocation',
    22. 'AMap.PlaceSearch',
    23. 'AMap.CitySearch',
    24. 'AMap.AutoComplete',
    25. 'AMap.Scale',
    26. 'AMap.ToolBar',
    27. 'AMap.MapType',
    28. 'AMap.DistrictSearch',
    29. ],
    30. AMapUI: {
    31. plugins: ['misc/PositionPicker'],
    32. },
    33. });
    34. }
    35. }
    36. export default AMapHelper;

              这里用class并不是让我们去生成AMapHelper实例,就算你生成实例,你也无法调用里面的static方法(静态方法无法被实例调用)。只能通过AMapHelper .getAMap 直接调用。为什么这么写呢?一是无需要实例化即可调用方法,节约了内存空间。二是无法被实例继承,也不会收到实例数据影响。保证了隐秘和私密性。

            当你定义的方法不需要通过 this 访问实例,换句话说并不依赖于具体的实例,并且和类强相关时,可以封装成类的静态方法。当然这个方法可以直接定义在类外面,只不过封装到类里面更符合开闭原则,直接点的好处就是通过类名访问静态方法不会污染外层环境

            像Math.max Date.now

            如果想深入了解,请看我这篇推文

    ES6 class类的静态方法static有什么用_AI3D_WebEngineer的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/weixin_42274805/article/details/133636906?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22133636906%22%2C%22source%22%3A%22weixin_42274805%22%7D

            这里的promiseLock是一个缓存promise结果的方法,我会在另外一篇推文里讲。这里知道是什么意思就行了。

            这里产出一个预加载了秘钥、版本、插件等信息的loader对象,并下放到全局window里方便调取。

              调用示例: 

    1. // xxx.vue
    2. import AMapHelper from '@/utils/amap';
    3. ...
    4. created() {
    5. if (!window.AMap) {
    6. await AMapHelper.getAMap();
    7. }
    8. this.aMapGeocoder = new window.AMap.Geocoder({
    9. // 批量查询
    10. batch: true,
    11. });
    12. }
    13. getCity() {
    14. const citySearch = new window.AMap.CitySearch();
    15. citySearch.getLocalCity((status, result) => {
    16. if (status === 'complete' && result.info === 'OK') {
    17. // 查询成功,result即为当前所在城市信息
    18. resolve(result);
    19. }
    20. });
    21. }

    方案一:echarts-extension-amap

            

            这是一个Apache ECharts 高德地图扩展。能够抓取高德地图作为echarts的画布。

    npm install echarts-extension-amap –save

            有了这个扩展之后,我们可以建立高德地图作为坐标系

    1. series:[
    2. {
    3. name: "PM2.5",
    4. type: "scatter",
    5. coordinateSystem: "amap",
    6. ...
    7. }
    8. ]

            引用:

    1. import * as echarts from "echarts";
    2. import "echarts-extension-amap";

            引入后就可以直接使用:

    1. var chart = echarts.init(document.getElementById("echarts-amap"));
    2. chart.setOption(option);
    3. // 从ECharts实例中取到高德地图组件实例
    4. var amap = chart.getModel().getComponent('amap').getAMap();
    5. // 下边就可以按照高德官方API随意调用了
    6. amap.addControl(new AMap.Scale());
    7. amap.addControl(new AMap.ToolBar());

            剩下的工作就是echarts的这个option该怎么写了,具体可以看插件文档。

    echarts-extension-amap/README.zh-CN.md at master · plainheart/echarts-extension-amap · GitHubicon-default.png?t=N7T8https://github.com/plainheart/echarts-extension-amap/blob/master/README.zh-CN.md        缺点:

            依赖第三方插件

            放大/下钻后整个画布冗余信息很多,比如街道、地标、高架、地貌等等,如果我们业务场景不需要关注这些信息,只需要大概到区级的统计。那么我们最好还是用第二种方法:

    方案二:GEO绘图

    介绍

            Echarts本身自带地理坐标系组件。并且支持geoJSON和SVG引入第三方资源。

    ​​        GeoJSON​​ 是一种数据格式。它被用来描述地理数据。

            GeoJSON是一种对各种地理数据结构进行编码的格式。
            GeoJSON对象可以表示几何(Geometry)、特征(Feature)或者特征集合
            GeoJSON支持下面几何类型:点、线、面、多点、多线、多面和几何集合。
            GeoJSON里的特征包含一个几何对象和其他属性,特征集合表示一系列特征。
            一个完整的GeoJSON数据结构可以称为一个对象。在GeoJSON里,对象由名/值对–也称作成员的集合组成。

            总而言之,GeoJSON是图形和描述的集合。

            就算是中国地图并不是一成不变的。行政区域时有改变,所以我推荐用geoJSON。

            如果预算不足,我们也可以直接白嫖阿里的GEO数据
    DataV.GeoAtlas地理小工具系列 (aliyun.com)icon-default.png?t=N7T8http://datav.aliyun.com/portal/school/atlas/area_selector        缺点是需要定期维护更新。

            下面讲讲我在项目中怎么用geoJSON去实现需求

    项目准备

            我把抓取下来的GEO数据(包括了中国、各省、各市、各区),生成以adCode命名的JSON保存下来。高德地图的adcode就是区域编号。

            类似于这样

            写个调取接口方便调取

    1. // chart.js
    2. export const getMapGeo = promiseLock(
    3. mapSrc =>
    4. uploadService.getHost().then(ossHost =>
    5. request(`${ossHost}/common/map/${mapSrc}.json`, {
    6. method: 'GET',
    7. }),
    8. ),
    9. {
    10. keyGenerator: mapSrc => mapSrc,
    11. forever: true,
    12. },
    13. );

    需求简述

            以中国地图展示数据分布,同时支持下钻到省、市,并有按钮可以返回。

            下钻:

    初始化

    1. import * as echarts from 'echarts';
    2. ...
    3. mounted() {
    4. const chartDom = this.$refs.chartDom;
    5. this.myChart = echarts.init(chartDom);
    6. this.initMap();
    7. }
    8. async initMap() {
    9. // 先调用封装的高德JS API loader
    10. await AMapHelper.getAMap();
    11. // 赋予组件内查询地理编码能力
    12. this.aMapGeocoder = new window.AMap.Geocoder({
    13. // 支持批量查询
    14. batch: true,
    15. });
    16. // 注册中国地图
    17. await echarts.registerMap('china', {
    18. name: 'china',
    19. level: 'country',
    20. // 从接口抓取china的GEOJSON
    21. geo: await getMapGeo('china'),
    22. });
    23. }

             因为业务需求需要下钻跟返回,所以我们需要像导航栏一样记录用户前进的路径。

    1. async initMap() {
    2. // 获取中国GEO
    3. this.mapHistory.push({
    4. name: 'china',
    5. level: 'country',
    6. geo: await getMapGeo('china'),
    7. });
    8. ...
    9. await echarts.registerMap('china', this.currentMap.geo);
    10. }

             这样初始化就完成了。此时echarts已经加载了第三方的China.json。"导航栏"已经记录了第一个全国地图的锚点。

            紧接着我们去调取数据。也就是地图上那些一个个点(这里以散点地图为例)的数据。拿到数据后,我们需要生成一张数据映射表。

    处理数据

            把数据的地理信息萃取出来,生成这么一个数组

    1. [
    2. ['广东省','江西省'...],
    3. ['广东省广州市','江西省南昌市'...],
    4. ['广东省广州市天河区','江西省南昌市东湖区'...]
    5. ]

            这里可以看到下标0都是省级单位,下标1是市级单位,下标2是区级单位。

            紧接着我们逐级去查询它们的地理编码数据。因为上面我们已经开启了batch批量查询,所以我们直接调用getLocationAPI(mapRecord[0])、getLocationAPI(mapRecord[1])、getLocationAPI(mapRecord[2])...

    1. // 调取高德API查询批量地理编码
    2. getLocationAPI(dataArr) {
    3. if (dataArr.length <= 10) {
    4. return new Promise((resolve, reject) => {
    5. this.aMapGeocoder.getLocation(dataArr, (status, result) => {
    6. if (status === 'error') {
    7. reject();
    8. } else {
    9. resolve(result);
    10. }
    11. });
    12. });
    13. }
    14. }

            虽然是批量查询,但是当数量超过10,高德的接口就限制了(要收费才支持更多每次查询量)。那么我的方法是:

    1. ...
    2. else {
    3. const promiseArr = [];
    4. // 当前指针
    5. let getIndex = 0;
    6. // 终点指针
    7. const finalIndex = dataArr.length - 1;
    8. dataArr.forEach((item, index) => {
    9. if (index > 1 && index % 10 === 0) {
    10. const searchArr = dataArr.filter(
    11. (t, i) => getIndex <= i && i < index,
    12. );
    13. promiseArr.push(
    14. new Promise((resolve, reject) => {
    15. this.aMapGeocoder.getLocation(searchArr, (status, result) => {
    16. if (status === 'error') {
    17. reject();
    18. } else {
    19. resolve(result);
    20. }
    21. });
    22. }),
    23. );
    24. getIndex = index;
    25. } else if (index === finalIndex) {
    26. const searchArr = dataArr.filter(
    27. (t, i) => getIndex <= i && i <= index,
    28. );
    29. promiseArr.push(
    30. new Promise((resolve, reject) => {
    31. this.aMapGeocoder.getLocation(searchArr, (status, result) => {
    32. if (status === 'error') {
    33. reject();
    34. } else {
    35. resolve(result);
    36. }
    37. });
    38. }),
    39. );
    40. }
    41. });
    42. return Promise.all(promiseArr).then(result => {
    43. let geocodes = [];
    44. result.forEach(item => (geocodes = geocodes.concat(item.geocodes)));
    45. return { geocodes };
    46. });
    47. }

            把查询数据切割成每十个一组,然后用promise.all来获取全部结果。

            获得结果后我用递归生成树状的映射表如下:

    1. this.mapTable = {
    2. 广东省: {
    3. name: '广东省',
    4. children: {
    5. 广州市: {
    6. name: '广州市',
    7. value:20,
    8. location: {xx,xx}
    9. children: {
    10. '天河区': {
    11. name: '广州市',
    12. value:10,
    13. location: {xx,xx},
    14. children: {}
    15. }
    16. }
    17. }
    18. },
    19. value: 10,
    20. location: { 30, l10 }, // 高德地图返回的经纬度
    21. },
    22. 江西省: {
    23. ...
    24. }
    25. }

            生成这张映射表是为了方便我们快速定位到锚点的层级数据和自身数据。

            看一下散点地图echarts的配置

    1. {
    2. ...,
    3. series: [
    4. {
    5. name: '数据',
    6. type: 'scatter',
    7. coordinateSystem: 'geo',
    8. data: []
    9. },
    10. },
    11. ],
    12. ],
    13. geo: {
    14. map: 'china',
    15. show: true,
    16. roam: true,
    17. aspectScale: 1,
    18. zoom: 1.5,
    19. center: [106.230909, 38.487193],
    20. }
    21. }

             地图组件加载完成后是以中国地图为版图的

            而series里type为scatter就是绘画地图上的散点,这样子散点图就完成了

            现在需要把省级的数据转成data

    1. // 把省份信息都拿出来
    2. Object.values(this.mapTable).map(item => ({
    3. name: item.name,
    4. value: item.value,
    5. selected: true,
    6. location: item.location,
    7. }));
    1. function formatterData(data, colorArr) {
    2. return data.map(item => ({
    3. name: item.name,
    4. value: [item.location.lng, item.location.lat, item.value],
    5. itemStyle: {
    6. color: colorArr[item.cLevel],
    7. },
    8. }));
    9. }

            生成类似这样的data数组:

    1. [
    2. {name:'广东省',value:[lng,lat,value]},
    3. {name:'江西省',value:[lng,lat,value]},
    4. ...
    5. ]

             传入series.data就行了

            如果要热力图或面积图,则:

    1. // 热力图
    2. series: [
    3. {
    4. name: '',
    5. type: 'heatmap',
    6. coordinateSystem: 'geo',
    7. data: [],
    8. roam: true,
    9. }
    10. ],
    11. // 视觉映射控件
    12. visualMap: {
    13. min: 0,
    14. max: 500,
    15. calculable: true,
    16. realtime: true,
    17. },

             面积图不需要经度 维度 也不用配置geo

            地区钻取         

            先响应echarts地图组件的双击事件

    1. this.myChart.on('dblclick', params => {
    2. this.gotoMapLevel(params);
    3. }

            从中国地图点击省份。这个params包括了:

             我们定义一个data属性来声明当前地图的层级,默认是country,下钻到省份是province,下钻到市区是city

    mapLevel = 'country';
    1. async gotoMapLevel(params) {
    2. // 区不展开详细地图
    3. if (this.mapLevel === 'city') return;
    4. // 双击事件被触发,必定是进入下一级地图
    5. const searchRes = this.searchInitNext(params.name)[0];
    6. // 这里是通过this.mapHistory最后一个元素的geo.features里面记录获取下一级信息
    7. this.mapLevel = searchRes.properties.level;
    8. this.mapHistory.push({
    9. name: searchRes.properties.name,
    10. level: this.mapLevel,
    11. geo: await getMapGeo(this.mapSourceUrl(searchRes.properties.adcode)),
    12. });
    13. this.rePaintMap();
    14. }

            mapSourceUrl我们前面已经讲过了,接口根据adcode拿到对应地区的geo数据。并把它存储到mapHistory里。那么在this.mapHistory既有层级level,也有geo数据。那么repaintMap就很简单了

    1. // 重绘地图
    2. async rePaintMap() {
    3. const nowArea = this.mapHistory[this.mapHistory.length - 1];
    4. this.mapLevel = nowArea.level;
    5. await echarts.registerMap(nowArea.name, nowArea.geo);
    6. ....
    7. }

            注册完geo,紧接着就是根据this,mapLevel找到映射数据去组装serise.data了 。之前那张数据映射表一下子就能帮忙了。

    1. get mapLevelGather() {
    2. switch (this.mapLevel) {
    3. case 'country':
    4. default:
    5. return Object.values(this.mapTable).map(item => ({
    6. name: item.name,
    7. value: item.value,
    8. selected: true,
    9. location: item.location,
    10. }));
    11. case 'province': {
    12. const nowArea = this.mapHistory[this.mapHistory.length - 1];
    13. if (!this.mapTable[nowArea.name]?.children) return [];
    14. return Object.values(this.mapTable[nowArea.name].children).map(
    15. item => ({
    16. name: item.name,
    17. value: item.value,
    18. location: item.location,
    19. }),
    20. );
    21. }
    22. case 'city': {
    23. const province = this.mapHistory[this.mapHistory.length - 2];
    24. const city = this.mapHistory[this.mapHistory.length - 1];
    25. if (!this.mapTable[province.name]?.children[city.name].children)
    26. return [];
    27. return Object.values(
    28. this.mapTable[province.name].children[city.name].children,
    29. ).map(item => ({
    30. name: item.name,
    31. value: item.value,
    32. location: item.location,
    33. }));
    34. }
    35. }
    36. }

            这不数据一下子就被筛选出来了,其他的和初始渲染一样啦

    返回上一级

    1. // 按钮
    2. returnBackMap() {
    3. if (this.mapLevel === 'province') {
    4. this.mapHistory.push(this.mapHistory[0]);
    5. } else {
    6. // 指针回到最后一级
    7. this.mapHistory.push(
    8. ...this.mapHistory.splice(this.mapHistory.length - 2, 1),
    9. );
    10. }
    11. this.rePaintMap();
    12. }

            有了this.mapHistory的geo和maplevel信息,就可以愉快的repaintMap了

    方案三:高德JS API绘制

         地图 JS API 2.0 是高德开放平台免费提供的第四代 Web 地图渲染引擎。本身便具有图层。

    1. var map = new AMap.Map('container', {
    2. center: [116.397428, 39.90923],
    3. layers: [//只显示默认图层的时候,layers可以缺省
    4. new AMap.TileLayer()//高德默认标准图层
    5. ],
    6. zoom: 13
    7. });

            只要调用API,就可以产出一个图层。

            高德地图还有多种图层可以使用,比如热力图层、视频图层(台风)。

    视频图层-自有数据图层-示例中心-JS API 示例 | 高德地图API (amap.com)icon-default.png?t=N7T8https://lbs.amap.com/demo/javascript-api/example/selflayer/videolayer        怎么用自己去看文档吧。

  • 相关阅读:
    第2-3-7章 个人网盘服务接口开发-文件存储服务系统-nginx/fastDFS/minio/阿里云oss/七牛云oss
    Spring整合Mybatis
    Docker网络模式之bridge-尚文网络xUP楠哥
    Vue动态样式绑定
    项目质量管理
    华为OD机试 - 机器人走迷宫 - 深度优先搜索dfs(Java 2023 B卷 200分)
    全志烧录工具LiveSuite使用注意事项
    搭建react-typescript-webpack开发环境
    处理机器学习数据集中字符串列(pandas.get_dummies)
    Linux安装Nexus(图文解说详细版)
  • 原文地址:https://blog.csdn.net/weixin_42274805/article/details/133636082