手摸手使用Vue3封装一个地图

9,807 阅读17分钟

开发背景

    入职这家公司之前,我是没有做过可视化项目的,最近可视化项目做的有点多,每一个可视化项目都不可避免地有一个地图,最初接手的可视化是Vue2 + 高德地图Api的,后来使用第三方可视化平台,而这个可视化平台使用的jsx语法封装组件,况且地图还要收费,对于我这种白嫖党绝对不能忍受!所以使用jsx又封装了一个地图组件,最近又有一个可视化项目,领导找我讨论继续用平台还是自己写,我义不容辞的选择了还是自己搞吧!然后技术选择了Vue3 + TypeScript + 高德地图Api,特来此分享一下整个实现流程。

NodeJS 版本使用的 16.18.0

项目构建

这里使用vite + vue3 + typescript开发

创建项目

首先进入你的一个空目录,打开终端,执行npm create vite@latest来创建项目。这里这个项目名就叫vue3-vite-gaode

image.png

这里项目名叫做vue3-vite-gaode,framework选择vue,语法选择TypeScript。回车之后项目就创建好了。然后使用vscode打开这个项目,你也可以选择别的编辑器。

定义组件

使用vscode打开项目之后,执行下面命令安装依赖,然后跑起项目。

# 安装依赖,如果你安装依赖比较慢的话,可以选择淘宝镜像,或者用yarn
npm i

# 跑起项目
npm run dev

跑起来之后,我们就可以使用这个链接访问

image.png

接下来我们删除掉模板里不需要的东西

  • src/components/HelloWorld.vue
  • App.vue里面的代码
  • src/style.css里面代码删除,加上body,html{ margin:0 }去除bodyhtml的默认margin

然后在src/components里面定义我们的Map组件,src/components/Map.vue

// src/components/Map.vue
<script setup lang="ts"></script>

<template></template>

<style scoped></style>

最后在App.vue引入这个Map

// App.vue
<script setup lang="ts">
import Map from "@/components/Map.vue";
</script>

<template>
  <Map />
</template>

<style scoped></style>

发现这里好像报错了!

image.png

这是TypeScript给我们报的错,因为他不知道@指向哪里,我们需要在tsconfig.json里面加上如下配置,告诉他基准路径@指向哪里就好了。

{
    "compilerOptions":{
        //...
        "baseUrl": ".",
        "paths": {
          "@/*": ["src/*"],
        }
    }
}

可是vscode终端仍然报错,同样vite不知道我们@指向哪里。

image.png

最后在vite.config.js里面添加如下配置,但你发现path又报错,原因是path这个模块是使用js编写的,而我们使用的是ts,包必须有ts的声明文件,这里需要再次执行npm install @types/node来安装nodets声明文件,为什么需要安装nodets声明文件,因为pathnode内置的。

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from "path";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    // 配置路径别名
    alias: {
      '@': path.resolve(__dirname, './src'),
    }
  },
})

到这里我们的Map组件已经定义好了,并在App.vue引入了,只是Map组件什么内容都没有。下一步我们将开始开发地图组件。

组件开发

第一阶段(显示地图)

首先肯定需要把地图能正常显示出来

  • 安装依赖

因为这里使用的是高德地图Api,所以先来安装依赖npm i @amap/amap-jsapi-loader

  • 初始化地图

要想显示地图,肯定需要一个容器,这里就使用一个id=mapdiv

<div id="map"></div>

并设置这个div满屏

#map{
    width:100vw;
    height:100vh;
}

接下来在js里面引入高德地图的AMapLoader,引入Vue生命周期onMountedref

<script setup lang="ts">
// 引入 onMounted 是因为地图一定是在页面挂载之后渲染,ref 为了定义响应式变量
import { onMounted, ref } from "vue";
import AMapLoader from "@amap/amap-jsapi-loader";
</script>

定义一个加载地图的方法,并在onMounted生命周期中调用

<script setup lang="ts">
// ...
const loadAMap = () => {
  AMapLoader.load({
    key: "你的高德地图Key",
    version: "2.0",
    plugins: [],// 你所使用到的插件
  }).then((AMap) => {
  
  });
};

onMounted(() => {
  loadAMap();
})
</script>

定义一个地图实例mapInstance,并在地图加载完成之后初始化地图

<script setup lang="ts">
// ...
const mapInstance = ref(null);

// 初始化地图
const initMapInstance = (AMap) => {
    // 生成地图的参数,具体可详见高德api文档
    const option:{[key:string]:any} = {};
    mapInstance.value = new AMap.Map("map", option);
};

// 在loadMap方法的then里面调用初始化地图
const loadAMap = () => {
  AMapLoader.load({
    key: "你的高德地图Key",
    version: "你的高德地图版本",
    plugins: [],// 你所使用到的插件
  }).then((AMap) => {
      initMapInstance(AMap);
  });
};
</script>

这时地图就已经正常显示了。

image.png

如果此时你的地图并没有显示出来,那你需要改造一下loadAMap方法。

const loadAMap = () => {
  window._AMapSecurityConfig = { 
      securityJsCode:"高德地图申请的key所附带的密钥"
  }
  // ...
};

第二阶段(显示指定区域的地图)

需求是实现暗色系,西安市的地图

主要的操作都是在new AMap.Mapoptions参数中处理

处理地图样式

这里我们定义一个props,用来扩展后面的参数

const props = defineProps({
  // 地图样式,需要实现暗色系,默认值就给成暗色系
  mapStyle: { type: String, default: "amap://styles/darkblue" },
})

// ...
// 在options中读取这个mapStyle
options = {
    mapStyle:props.mapStyle
}

暗色系地图就实现了,至于为什么是这个值,可以参考官方文档高德地图官方API之地图主题这里面也提供了自定义地图主题方法。

image.png

仅显示西安市地图

这里需要给props扩展属性,将我们的加载地图时的keyversion等写到props里面,定义为apiConfig, 同时扩展plugins,定义一个属性名areaName,值为需要显示的区域名,这里默认为西安市,增加实例化AMap.DistrictSearch对象是需要的属性(levelextensionssubdistrict

// ...
const props = defineProps({
  // 地图样式,需要实现暗色系,默认值就给成暗色系
  mapStyle: { type: String, default: "amap://styles/darkblue" },
 // 地图配置
 apiConfig:{ type: Object, default:() => ({
   version:"2.0",
   key:"你所申请的高德key",
   plugins:["AMap.DistrictSearch"]
 }) },
 // 区域名
 areaName:{ type:String, default:"西安市" },
 // 显示下级行政区级数,行政区级别包括:国家、省/直辖市、市、区/县4个级别
 subdistrict:{ type:Number, default:0 },
 // 是否返回行政区边界坐标点 all / base
 extensions:{ type:String,default:"all" },
 // 搜索范围[对应文档https://lbs.amap.com/api/javascript-api/reference/search#m_AMap.DistrictSearch]
  level:{ type:String,default:"city" }
})

将加载地图时写死的keyversion改为读取props属性

// ...
AMapLoader.load({
  key:props.apiConfig.key,
  version:props.apiConfig.version,
  plugins:props.apiConfig.plugins
}).then(AMap => {
  initMapInstance(AMap);
})

改写初始化地图方法,需要先获取区域的边界坐标点,然后再初始化地图。

const initMapInstance = (AMap:any) => {
  const options:{
    [key:string]:any
  } = {
    mapStyle:props.mapStyle,
  };

  // 初始化district对象
  const district = new AMap.DistrictSearch({
    level:props.level,
    extensions:props.extensions,
    subdistrict:props.subdistrict,
  });

  // 搜索区域
  district.search(props.areaName, function (status, result) {
    
    const bounds = result.districtList[0]["boundaries"];

    // 获取区域各坐标
    const mask = [];
    for (let i = 0; i < bounds.length; i += 1) {
      mask.push([bounds[i]]);
    }
    
    // options中设置mask,超出mask的区域就不显示
    options.mask = mask;
    
    mapInstance.value = new AMap.Map("map", options);
  })
};

这时地图就已经只显示西安市的了

image.png

但是有个问题我相信大家已经发现了,这个网格背景怎么去掉?

只需要在css中加入这么一行代码就去掉了背景网格。

.amap-container{
  background-image: unset;
}

这个时候就成功只显示了西安市的地图

image.png

但是总感觉有点奇怪,现在我们来给这个地图加个描边。

props里面添加一个边界配置属性polylineConfig

// ...
const props = defineProps({
  // ...
  polylineConfig: {
    type: Object,
    default: () => ({
      // 是否显示边界线
      show: true,
      // 是否显示边界以外的区域
      showOuter: false,
      // 边界线条颜色
      strokeColor: "#99ffff",
      // 边界线条粗细
      strokeWeight: 4,
    }),
  },
})

同样再改写一下初始化地图方法

// 新增一个渲染边界的方法
const renderPolyLine = (bounds = []) => {
  const { polylineConfig } = props;

  if (polylineConfig.show) {
    for (let i = 0; i < bounds.length; i++) {
      new AMap.Polyline({
        path: bounds[i],
        strokeColor: polylineConfig.strokeColor,
        strokeWeight: polylineConfig.strokeWeight,
        map: mapInstance.value,
      });
    }
  }
};

// 初始化地图
const initMapInstance = (AMap:any) => {
  const options:{
    [key:string]:any
  } = {
    mapStyle:props.mapStyle,
  };

  // 初始化district对象
  const district = new AMap.DistrictSearch({
    level:props.level,
    extensions:props.extensions,
    subdistrict:props.subdistrict,
  });

  // 搜索区域
  district.search(props.areaName, function (status, result) {
    
    const bounds = result.districtList[0]["boundaries"];

    // 获取区域各坐标
    const mask = [];
    for (let i = 0; i < bounds.length; i += 1) {
      mask.push([bounds[i]]);
    }
    
    // 不显示区域外位置
    if (!props.polylineConfig.showOuter) {
      options.mask = mask;
    }
    
    mapInstance.value = new AMap.Map("map", options);
    
    // 渲染边界
    renderPolyLine(bounds);
  })
};

这个时候我们地图就好看多了

image.png

到这里为止,我们地图只显示指定区域就结束了,后面我们将逐步扩展,使得地图更加丰富。

第三阶段(新增需求)

需求一

  • 支持鼠标双击放大缩小
  • 支持设置中心点
  • 支持设置缩放范围和初始缩放等级

别看这么多需求,每个都只有一行代码。

先在props中将这些属性都纷纷配置。

const props = defineProps({
  // ...
  // 地图是否支持双击鼠标放大
  doubleClickZoom: { type: Boolean, default: true },
  // 中心点坐标
  center: { type: Array, default: () => [108.939677,34.3432] },
  // 初始地图缩放等级
  zoom: { type: Number, default: 10 },
  // 地图显示的缩放级别范围
  zooms: { type: Array, default: () => [3, 18] },
})

然后在初始化地图时加入到options

const initMapInstance = (AMap:any) => {
  const options:{
    [key:string]:any
  } = {
    mapStyle:props.mapStyle,
    doubleClickZoom:props.doubleClickZoom,
    center:props.center,
    zoom:props.zoom,
    zooms:props.zooms
  };
   
  // ...
};

就这!上述需求都已实现!

20230318153851.gif

需求二

  • 展示卫星地图
  • 展示卫星路网

同样我们先在props里面配置是否展示卫星地图isShowSatellite是否展示卫星路网isShowRoadNet

const props = defineProps({
  // ...
  // 是否展示卫星地图
  isShowSatellite: { type: Boolean, default: true },
  // 是否展示卫星路网
  isShowRoadNet: { type: Boolean, default: true },
})

修改初始化地图方法

const initMapInstance = (AMap:any) => {
  const options:{
    [key:string]:any
  } = {
    mapStyle:props.mapStyle,
    doubleClickZoom:props.doubleClickZoom,
    center:props.center,
    zoom:props.zoom,
    zooms:props.zooms,
    // 图层,卫星地图,卫星路网都属于图层,push到这个layers就可以了
    layers:[]
  };
  
  // 展示卫星图层
  if (props.isShowSatellite) {
    option.layers.push(new AMap.TileLayer.Satellite());
  }

  // 展示路网图层
  if (props.isShowRoadNet) {
    option.layers.push(new AMap.TileLayer.RoadNet());
  }
  
  // ...
};

卫星路网,卫星地图也都正常显示了

image.png

但是这种地图我觉得不好看,经过一番辩论之后,还是在props中将他设为false,换回正常的地图。

需求三

  • 要实现3D效果
  • 并且可以调整俯视角度
  • 还要有地图方位控制器

继续在props中添加属性

const props = defineProps({
  // 这里用到了地图方位控制器插件,所以需要在apiConfig的plugins中引入
  apiConfig:{ type: Object, default:() => ({   
    version:"2.0", 
    key:"你所申请的高德key", 
    plugins:["AMap.DistrictSearch","AMap.ControlBar"] 
  }) },
  
  // ...
  
  // 是否3D显示
  isShow3D: { type: Boolean, default: true },
  // 俯视角度
  pitch: { type: Number, default:40 },
  // 地图方位控制器配置
  controllBarConfig: {
    type: Object,
    default: () => ({
      // 是否显示方位控制器
      show: true,
      // 是否显示缩放按钮
      showZoomBar: true,
      // 是否显示倾斜、旋转按钮
      showControlButton: true,
      // 距离顶部的距离
      positionTop: 10,
      // 距离右侧的距离
      positionRight: 10,
    }),
  },
  // 3d墙体配置
  object3dWallConfig: {
    type: Object,
    default: () => ({
      // 是否显示3d墙体
      show: true,
      // 层级
      zIndex: 1,
      // 墙高
      wallHeight: -4000,
      // 墙体颜色
      color: "#0088ffcc",
      // 是否使用了透明颜色,并进行颜色混合
      transparent: true,
      // 控制显示正反面,both,front,back
      backOrFront: "both",
    }),
  },
})

老样子,继续在地图初始化函数中处理,但是3D墙体只有在1.4.15低版本API中才有,不得已又把props.apiConfig.version 换回了1.4.15

// 定义一个渲染3d墙体的方法
const render3dWall = (bounds = []) => {
  const { object3dWallConfig, apiConfig } = props;

  // 1.4.15版本的api通过Object3DLayer创建墙体
  if (apiConfig.version == "1.4.15") {
    if (object3dWallConfig.show) {
      // 定义一个3D图层
      const object3Dlayer = new AMap.Object3DLayer({
        zIndex: object3dWallConfig.zIndex,
      });
      // 创建墙体
      const wall = new AMap.Object3D.Wall({
        path: bounds,
        height: object3dWallConfig.wallHeight,
        color: object3dWallConfig.color,
      });
      wall.transparent = object3dWallConfig.transparent;
      wall.backOrFront = object3dWallConfig.backOrFront;
      object3Dlayer.add(wall);
      mapInstance.value.add(object3Dlayer);
    }
  } else if (apiConfig.version == "2.0") {
    // 2.0版本的api通过描边添加墙体
    for (let i = 0; i < bounds.length; i += 1) {
      new AMap.Polyline({
        path: bounds[i],
        strokeColor: object3dWallConfig.color,
        strokeWeight: object3dWallConfig.wallHeight,
        map: mapInstance.value,
      });
    }
  }
};

// 定义一个渲染地图方位控制器
const renderControlBar = () => {
  const { controllBarConfig } = props;
  if (controllBarConfig.show) {
    mapInstance.value.addControl(
      new AMap.ControlBar({
        showZoomBar: controllBarConfig.showZoomBar,
        showControlButton: controllBarConfig.showControlButton,
        position: {
          right: `${controllBarConfig.positionRight}px`,
          top: `${controllBarConfig.positionTop}px`,
        },
      }),
    );
  }
};

// 初始化地图
const initMapInstance = (AMap:any) => {
  const options:{
    [key:string]:any
  } = {
    mapStyle:props.mapStyle,
    doubleClickZoom:props.doubleClickZoom,
    center:props.center,
    zoom:props.zoom,
    zooms:props.zooms,
    // 图层,卫星地图,卫星路网都属于图层,push到这个layers就可以了
    layers:[],
    // 俯仰角度,默认0,[0,83],2D地图下无效
    pitch: props.pitch,
  };
  
  // 3D显示,地图风格,3D还是2D
  if (props.isShow3D) {
    options.viewMode = "3D";
  }
  
  // ...
  
  // 添加3D墙体  高德地图api1.4.15生效
  render3dWall(bounds);
  
  // 添加地图方位控制器
  renderControlBar();
};

又加了这一坨屎山,终于完成了这个需求。

20230318161754.gif

需求四

  • 绘制线条

要想实现绘制线条,我们首先得有线条数据,而每一条线条数据都是由一堆点位集合组成,我这里事先准备西安市的一些点位。

// 第二维代表线   第三维代表点
[
    [
        [108.771259,34.210635],
        [108.827026,34.190093],
        [108.880493,34.199648]
    ],
    [
        [108.895441,34.241677],
        [108.951208,34.230217]
    ]
]

然后我们定义一个渲染线条的方法,将线条数据循环添加到地图上。

// 渲染线条
const renderLine = () => {
  const polyLineData = [
    [
      [108.771259,34.210635],
      [108.827026,34.190093],
      [108.880493,34.199648]
    ],
    [
      [108.895441,34.241677],
      [108.951208,34.230217]
    ]
  ];
  const polyLines = [];
  for (let i = 0; i < polyLineData.length; i++) {
    const polyline = new AMap.Polyline({
      // 线条坐标
      path: polyLineData[i],
    });
    polyLines.push(polyline);
  }

  mapInstance.value.add(polyLines);
};

然后继续在initMapInstance方法中去调用渲染线条的方法。

// 初始化地图
const initMapInstance = (AMap:any) => {
  // ...
  renderLine()
};

此时我们的线条就已经有了。

image.png

但是这也太不明显了吧,我们还要实现线条的样式控制。

这时我们就要考虑将他加入到props里了,首先接收一个布尔类型isDrawPolyLine用来控制是否绘制线条,然后接收一个数组polyLineData,数组的每一项都是一条线,数组里面的path指的是这条线由哪些点连成,点的顺序不同线条也就不同,其他属性都是线条的样式控制。

const props = defineProps({
  // ...
  // 是否绘制线条
  isDrawPolyLine:{ type:Boolean,default:true },
  // 线条数据
  polyLineData: {
    type: Array,
    default: () => [
      {
        path: [
          [108.771259,34.210635],
          [108.827026,34.190093],
          [108.880493,34.199648]
        ],
        // 是否显示描边
        isOutline: true,
        // 线条描边颜色
        outlineColor: "#ffeeff",
        // 描边的宽度
        borderWeight: 3,
        // 线条颜色
        strokeColor: "#3366FF",
        // 线条透明度,取值范围[0,1],默认0.9
        strokeOpacity: 1,
        // 线条宽度
        strokeWeight: 6,
        // 线条样式,实线:solid,虚线:dashed
        strokeStyle: "solid",
        // 勾勒形状轮廓的虚线和间隙的样式
        strokeDasharray: [10, 5],
        // 折线拐点的绘制样式,默认值为'miter'尖角,其他可选值:'round'圆角、'bevel'斜角
        lineJoin: "round",
        // 折线两端线帽的绘制样式,默认值为'butt'无头,其他可选值:'round'圆头、'square'方头
        lineCap: "round",
        // 折线覆盖物的叠加顺序
        zIndex: 50,
      },
      {
        path:[
          [108.895441,34.241677],
          [108.951208,34.230217]
        ],
        // 是否显示描边
        isOutline: true,
        // 线条描边颜色
        outlineColor: "#ffeeff",
        // 描边的宽度
        borderWeight: 3,
        // 线条颜色
        strokeColor: "#3366FF",
        // 线条透明度,取值范围[0,1],默认0.9
        strokeOpacity: 1,
        // 线条宽度
        strokeWeight: 6,
        // 线条样式,实线:solid,虚线:dashed
        strokeStyle: "solid",
        // 勾勒形状轮廓的虚线和间隙的样式
        strokeDasharray: [10, 5],
        // 折线拐点的绘制样式,默认值为'miter'尖角,其他可选值:'round'圆角、'bevel'斜角
        lineJoin: "round",
        // 折线两端线帽的绘制样式,默认值为'butt'无头,其他可选值:'round'圆头、'square'方头
        lineCap: "round",
        // 折线覆盖物的叠加顺序
        zIndex: 50,
      },
    ],
  }
})

接下来我们修改一下渲染线条的方法。

const renderLine = () => {
  const { isDrawPolyLine, polyLineData }:any = props;

  if (isDrawPolyLine) {
    const polyLines = [];
    for (let i = 0; i < polyLineData.length; i++) {
      const polyline = new AMap.Polyline({
        // 线条坐标
        path: polyLineData[i].path,
        // 是否显示外线
        isOutline: polyLineData[i].isOutline,
        // 外线颜色
        outlineColor: polyLineData[i].outlineColor,
        // 外线宽度
        borderWeight: polyLineData[i].borderWeight,
        // 线条颜色
        strokeColor: polyLineData[i].strokeColor,
        // 线条透明度
        strokeOpacity: polyLineData[i].strokeOpacity,
        // 线条宽度
        strokeWeight: polyLineData[i].strokeWeight,
        // 线条样式,实线:solid,虚线:dashed
        strokeStyle: polyLineData[i].strokeStyle,
        // 勾勒形状轮廓的虚线和间隙的样式
        strokeDasharray: polyLineData[i].strokeDasharray,
        // 折线拐点的绘制样式
        lineJoin: polyLineData[i].lineJoin,
        // 折线两端线帽的绘制样式
        lineCap: polyLineData[i].lineCap,
        //  折线覆盖物的叠加顺序
        zIndex: polyLineData[i].zIndex,
      });
      polyLines.push(polyline);
    }

    mapInstance.value.add(polyLines);
  }
};

这时地图上的线条就这样了,emmm...好像有点丑,但我的审美好像就到这了,读者可以自行优化。

image.png

需求五

  • 增加点位

有了线条,怎么少得了点位呢?

首先,在props里面加入是否渲染点位的布尔值和点位数据。

const props = defineProps({
  // 是否绘制点位
  isDrawPoint: { type: Boolean, default: true },
  // 点位数据
  pointData: {
    type: Array,
    default: () => [
      {
        // 唯一值
        iden: "点位1",
        // 坐标
        lngLat: [108.979378,34.221143],
        // marker点位基于坐标的偏移量
        offset: [-13, -30],
        // 自定义图标(Object可设置精灵图定位,String为图标地址)
        icon: {
          // 图标大小
          size: [25, 34],
          // 图标地址
          image:
            "//a.amap.com/jsapi_demos/static/demo-center/icons/dir-marker.png",
          // 图标所用的图片大小
          imageSize: [135, 40],
          // 图标取图偏移量(背景图定位)
          imageOffset: [-9, -3],
        },
      },
      {
        // 唯一值
        iden: "点位2",
        // 坐标
        lngLat: [108.896591,34.255523],
        // marker点位基于坐标的偏移量
        offset: [-13, -30],
        // 自定义图标(Object可设置精灵图定位,String为图标地址)
        icon: "//a.amap.com/jsapi_demos/static/demo-center/icons/dir-via-marker.png",
      },
      {
        // 唯一值
        iden: "点位3",
        // 坐标
        lngLat: [109.065616,34.328056],
        // marker点位基于坐标的偏移量
        offset: [-13, -30],
        // 自定义图标(Object可设置精灵图定位,String为图标地址)
        icon: {
          // 图标大小
          size: [25, 34],
          // 图标地址
          image:
            "//a.amap.com/jsapi_demos/static/demo-center/icons/dir-marker.png",
          // 图标所用的图片大小
          imageSize: [135, 40],
          // 图标取图偏移量(背景图定位)
          imageOffset: [-95, -3],
        },
      },
    ],
  }
})

然后继续定义一个渲染点位的方法,每一个点位其实就是高德里面的一个Maker对象,通常,点位是可以被点击的,也就给所有的Maker添加点击事件,这里就不细讲了,可以拿到当前Maker的数据,通过Vueemit传给父组件,点击事件在父组件去处理。

// 渲染点位
const renderPoint = () => {
  const { isDrawPoint, pointData }:any = props;

  if (isDrawPoint) {
    const makers = [];

    for (let i = 0; i < pointData.length; i++) {
      // 定义图标
      let icon = pointData[i].icon;
      if (typeof pointData[i].icon !== "string") {
        icon = new AMap.Icon({
          // 图标尺寸
          size: new AMap.Size(...pointData[i].icon.size),
          // 图标的取图地址
          image: pointData[i].icon.image,
          // 图标所用图片大小
          imageSize: new AMap.Size(...pointData[i].icon.imageSize),
          // 图标取图偏移量
          imageOffset: new AMap.Pixel(...pointData[i].icon.imageOffset),
        });
      }
      // 定义maker
      const maker = new AMap.Marker({
        position: new AMap.LngLat(...pointData[i].lngLat),
        offset: new AMap.Pixel(...pointData[i].offset),
        icon,
      });
      // 点位添加点击事件
      maker.on("click", function () {
        alert(pointData[i])
      });
      // 添加maker
      makers.push(maker);
    }
    // 添加点位到地图
    mapInstance.value.add(makers);
  }
};

最后在初始化地图之后再次调用。

const initMapInstance = (AMap:any) => {
    //...
    
    // 渲染点位
    renderPoint();
}

这个时候发现了个问题,通过控制台可以发现,它的每一个Marker都是一个Dom对象,假设我们有成百上千个点,那一般电脑很难渲染得动,所以免不了卡的不行。这个时候我们将点位Marker对象更改为海量点MassMarks对象。

修改renderPoint方法:

// 渲染点位
const renderPoint = () => {
  const { isDrawPoint, pointData }:any = props;

  if (isDrawPoint) {
    // 将里面的代码通通注释掉 ...
    
    // styles存储所有的图标样式
    const styles = [];
    // 为了兼容原来的方式,构造一个新对象存储在styles
    // anchor为图标在地图上的偏移量,url等同于Marker对象的Icon的image
    pointData.forEach((point:any) => !(styles.find(icon => icon.image == point.icon.image)) 
        && styles.push({...point.icon, anchor:new AMap.Pixel(15,35), url:point.icon.image}));
    
    // 构造新的点位集合,因为MassMarks对象的点位中必须有lnglat经纬度
    // 与Marker对象的经纬度区别在于Lat的L要小写
    // style为当前点位的图标样式在styles中的索引
    const points = pointData.map( (point:any) => ({ ...point,lnglat: point.lngLat, style:styles.findIndex(icon => icon.url === point.icon.image) }) );
    
    // 生成一个海量点图层,第一个参数为点位数据,第二个参数为一个options,选项样式
    const massMarks = new AMap.MassMarks(points,{
      zIndex:114,
      zooms:[3,19],
      style:styles
    })
    
    // 将当前海量点图层添加到地图上
    massMarks.setMap(mapInstance.value);
    
    // 点位添加点击事件
    massMarks.on("click", function ({ data }) {
      alert(data)
    });
  }
};

这个时候我们无论有几千个点位,都不会造成多个dom节点的渲染,而引起浏览器卡爆,nice!

image.png

当然,这个点位的图标你是可以自行配置的,可以让你们的UI设计一些图标供你使用,或者在网上找一些图标都是可以的,我这里用的是高德官方的图标。

需求六

  • 增加脉冲线

先把之前的点位和线条关掉。

然后再次在props里面添加属性,脉冲线的配置和数据。脉冲线我们需要用到高德地图的LOCAL数据可视化API,所以需要加上LOCAL的配置,其次还有脉冲线的数据及样式配置。

const props = defineProps({
  // 地图Loca配置
  locaConfig: {
    type: Object,
    default: () => ({
      // 是否展示
      show: true,
      // 资源类型 url(geoJson地址) data(geoJson数据)
      sourceType: "data",
      // 缓冲线脚本版本号,目前是基于2.0开发的
      version: "2.0",
    }),
  },
  // 脉冲线数据
  locaData: {
    type: Object,
    default: () => ({
      // 当脉冲线sourceType为url时必传
      geoJsonUrl: "",
      // 当脉冲线sourceType为data时必传
      geoJsonData: {
        type: "FeatureCollection",
        features: [
          {
            type: "Feature",
            // id保持唯一,如果用其他平台获取geoJson数据的话自带有id
            id: 3657,
            // properties里面的属性可以自由扩展
            properties: { _draw_type: "line" },
            geometry: {
              type: "LineString",
              // 脉冲线由哪些点位坐标组成
              coordinates: [
                [108.735039,34.16429],
                [108.929361,34.120309],
                [109.085738,34.179582],
                [108.927636,34.217322],
              ],
            },
            // 脉冲头和脉冲尾的坐标
            bbox: [108.735039,34.16429, 108.927636,34.217322],
          },
          {
            type: "Feature",
            id: 4901,
            properties: { _draw_type: "line" },
            geometry: {
              type: "LineString",
              coordinates: [
                [108.769534,34.286071],
                [108.979953,34.298001],
                [109.121958,34.187705],
                [108.894866,34.20968],
              ],
            },
            bbox: [108.769534,34.286071, 108.894866,34.20968],
          },
        ],
      },
      // 脉冲线图层样式
      globalStyle: {
        // 图层显示层级
        zIndex: 10,
        // 图层整体透明度
        opacity: 1,
        // 图层是否可见
        visible: true,
        // 图层缩放等级[0-20]
        zooms: [2, 22],
      },
      // 脉冲线样式
      layerStyle: {
        // 线整体海拔高度,Number
        altitude: 0,
        // 脉冲线的宽度
        lineWidth: 10,
        // 脉冲头颜色
        headColor: "rgba(227,43,43,0.4)",
        // 脉冲尾颜色
        trailColor: "rgba(0,0,0, 0)",
        // 脉冲长度,0.25 表示一段脉冲占整条路的 1/4
        interval: 0.75,
        // 脉冲线的速度,几秒钟跑完整段路
        duration: 2000,
      },
    }),
  },
})

然后我们需要定义一个变量,用来初始化LOCAL

const locaInstance = ref(null);

在加载地图时也要加载Loca

const loadAMap = () => {
  AMapLoader.load({
    key:props.apiConfig.key,
    version:props.apiConfig.version,
    plugins:props.apiConfig.plugins,
    // 加载Loca脉冲线才有效果
    Loca: {
      version: props.locaConfig.version,
    },
  }).then(AMap => {
    initMapInstance(AMap);
  })
}

定义一个渲染脉冲线的方法。

// 渲染脉冲线
const renderLoca = () => {
  const { locaConfig, locaData } = props;

  // 未开启脉冲线
  if (!locaConfig.show) return;

  // 初始化脉冲线容器
  locaInstance.value = new Loca.Container({
    map: mapInstance.value,
  });

  // 获取geoJson数据
  const sourceParams = {};
  // sourceType与data.locaData的key的映射关系
  const sourceTypeToDataKey = {
    url: "geoJsonUrl",
    data: "geoJsonData",
  };
  sourceParams[locaConfig.sourceType] =
    locaData[sourceTypeToDataKey[locaConfig.sourceType]];
  // 读取指定资源
  const geo = new Loca.GeoJSONSource(sourceParams);
  // 添加脉冲线图层
  const layer = new Loca.PulseLineLayer({
    loca: locaInstance.value,
    // 图层显示层级
    zIndex: locaData.globalStyle.zIndex,
    // 图层整体透明度
    opacity: locaData.globalStyle.opacity,
    // 图层是否可见
    visible: locaData.globalStyle.visible,
    // 图层缩放等级[0-20]
    zooms: locaData.globalStyle.zooms,
  });

  // 将geoJson数据加载给脉冲线图层
  layer.setSource(geo);

  // 设置脉冲线样式
  layer.setStyle({
    // 线整体海拔高度,Number
    altitude: locaData.layerStyle.altitude,
    // 脉冲线的宽度
    lineWidth: locaData.layerStyle.lineWidth,
    // 脉冲头颜色locaData.layerStyle.headColor
    headColor: "#ff0000",
    // 脉冲尾颜色
    trailColor:"#0099ff",
    // 脉冲头和脉冲尾的值可以是个回调函数,回调函数里面的第二个参数就能拿到你的geoJson数据项,可以根据里面的唯一值来取对应的颜色
    // trailColor: (_, feature) =>
      feature.properties.type
        ? DeviceTypeToLocaLayerTrailColor[feature.properties.type]
        : locaData.layerStyle.trailColor,
    // 脉冲长度,0.25 表示一段脉冲占整条路的 1/4
    interval: locaData.layerStyle.interval,
    // 脉冲线的速度,几秒钟跑完整段路
    duration: locaData.layerStyle.duration,
  });

  // 添加脉冲线图层到脉冲线容器
  locaInstance.value.add(layer);

  // 开始动画
  locaInstance.value.animate.start();
};

最后将这个方法在地图初始化initMapInstance完成之后调用。

const initMapInstance = (AMap:any) => {
    //...
    // 渲染脉冲线
    renderLoca();
}

最后效果就是这样子,线条的点位顺序决定了脉冲线动画走向。这里都是随机点的几个点位。

1.gif

需求七

  • 增加预警点位

继续在props里面加入是否展示预警点位isShowPointWarning和预警点位集合warningList

const props = defineProps({
    // 是否显示预警点位动画
    isShowPointWarning: { type: Boolean, default: true },
    // 预警点位集合
    warningList: {
      type: Array,
      default: () => [[108.777008,34.216845]],
    },
})

然后定义一个渲染预警点位的方法

// 引入红色预警的图片
import breathRedPng from "./assets/breath_red.png";

//处理动画点位所需的json
const aniPointJsonData = () => {
  const features = props.warningList.map((coordinates) => ({
    type: "Feature",
    geometry: { type: "Point", coordinates },
  }));
  return {
    data: { type: "FeatureCollection", features },
  };
};

// 渲染动态动画点位
const renderAniPoint = () => {
  const { isShowPointWarning } = props;

  // 红色呼吸点
  if (isShowPointWarning) {
    const geoLevelF = new Loca.GeoJSONSource(aniPointJsonData());
    const breathRed = new Loca.ScatterLayer({
      loca: locaInstance.value,
      // 图层显示层级
      zIndex: 113,
      // 图层整体透明度
      opacity: 1,
      // 图层是否可见
      visible: true,
      // 图层缩放等级范围
      zooms: [2, 22],
    });

    breathRed.setSource(geoLevelF);
    breathRed.setStyle({
      // size 和 borderWidth 的单位,可以是 'px' 和 'meter',meter 是实际地理的米,px 是屏幕像素。
      unit: "meter",
      // 图标长宽,单位取决于 unit 字段
      // 这里有个小细节,如果unit设置为meter的话,鼠标滚动放大地图,预警点位也会放大
      // 如果我们想要放大地图,预警点位缩小,uni可以使用px,使用固定大小的像素,size设置像素大小值
      size: [4000, 4000],
      // 图标纹理资源
      texture: breathRedPng,
      // 一轮动画的时长,单位毫秒(ms)
      duration: 500,
      // 是否有动画
      animate: true,
    });

    // 启动渲染动画
    locaInstance.value.animate.start();
  }
};

最后将这个方法在地图初始化initMapInstance完成之后调用,需要注意的是,同样依赖于LOCAL,如果你前面没有做脉冲线的话,需要先实例化LOCAL再使用预警点位的api。

const initMapInstance = (AMap:any) => {
    //...
    // 渲染预警点位
    renderAniPoint();
}

最后效果是这样

1.gif

结语

历时两周(周末),完成这篇文章,整个Demo也是自己独立完成,希望这篇文章能让你学会使用高德API去做一些好玩的东西,如果能够帮助到你,我自然也很开心,最后希望大家一起进步。如果不嫌弃可以稍微点个赞!!!

稍后我会将这个地图组件的源码开源到Gitee,你只需要换上你自己的高德Key,并传入你需要的参数即可使用,示例中的参数值数据什么的都是放在propsdefault中,你可以自行删除!!!