tony的高德地图api爬坑之路...

2,078 阅读7分钟

前言

由于项目里面涉及到高德地图web端相关业务需求,自己也是从不会到看文档慢慢学会,诚惶诚恐,这篇文章与其说是技术分享,不如说是给没接触过高德地图jsapi的前端开发者分享一些小小的建议以及自己的一些经验之谈,说的有不对的地方欢迎大家指正~

接下来我会从【新手怎么看文档】【怎么根据业务需求合理使用api】【怎么封装业务组件】【那些年Tony爬过的坑】这几个方面来具体聊一聊

1.新手怎么看文档

个人认为高德地图api文档相对百度地图、bing地图而言更加清晰明了,对新手比较友好,我拿到需求如果不熟悉也要先看一下文档,只需要看web端jsapi即可,主要看这几个板块:

  1. 示例中心(这里有一些前人写好的demo,拿来稍微改动一下就可以变成业务代码)
  2. 参考手册(如果示例中心还不能满足需求,或者对某个类某个方法不熟悉,可以移步到这里定位到相关类或者方法仔细观察一下)
  3. AMapUi组件(这个是高德自己封装好的一些组件,可以实现一些复杂业务,比如用巡航器结合el-slider组件实现带进度条的轨迹回放效果)

2.怎么根据业务需求合理使用api

2.1.地图

2.2.1.创建地图

我一般使用vue-amap插件引入高德地图,首先安装插件npm i vue-amap -S

main.js
// 引入vue-amap
import VueAMap from 'vue-amap';
Vue.use(VueAMap);
// 初始化vue-amap
VueAMap.initAMapApiLoader({
  // 高德的key
  key: '你申请的key',
  // 插件集合
  plugin: ['Autocomplete', 'PlaceSearch', 'Scale', 'HawkEye', 'ToolBar', 'ControlBar', 'MapType', 'PolyEditor', 'AMap.CircleEditor', 'AMap.MarkerClusterer', 'AMap.Geocoder'],
  // api版本
  v: '2.0',
  // AMapUi组件库版本
  uiVersion: '1.1.1'
});

单文件组件内使用
import { lazyAMapApiLoaderInstance } from 'vue-amap'

initMap () {
      lazyAMapApiLoaderInstance.load().then(() => {
        this.map = new AMap.Map('map', {
          viewMode: '2D',
          center: this.center,
          zoom: this.zoom
        })
        let toolBar = new AMap.ToolBar({
          position: {
            top: '110px',
            left: '40px'
          }
        })
        let controlBar = new AMap.ControlBar({
          position: {
            top: '10px',
            left: '10px'
          }
        })
        let scale = new AMap.Scale()
        this.map.addControl(toolBar)
        this.map.addControl(controlBar)
        this.map.addControl(scale)
      })
    }  // 这个是2.0版本写法,1.x版本写法稍微有所不同

2.2.点标记

2.2.1.创建点标记

点击按钮给地图绑定点击事件--->根据事件对象获取点击位置经纬度--->将经纬度传入AMap.Marker构造函数---> new实例化AMap.Marker构造函数--->调用map的add方法将生成的实例渲染到地图上 或者 直接使用AMap.MouseTool鼠标绘制工具类 建议使用第二种方法,用户体验比较好

2.2.2.点标记添加弹跳动画

需要注意的是如果使用1.4.15版本api直接调用相关方法即可,如果使用2.0版本ap好像需要自己实现,实现代码如下:

/deep/.bounce-marker {
          animation: bounce 0.5s cubic-bezier(0.1, 0.25, 0.1, 1) 0s infinite
            alternate both;
        }
        @keyframes bounce {
          from {
            transform: translateY(0px);
          }
          to {
            transform: translateY(-40px);
          }
        }

2.2.3.点标记轨迹回放

这个参考文档即可,2.0版本写法和1.x版本有所不同

2.2.4.点标记聚合显示

这个参考文档即可,2.0版本写法和1.x版本有所不同

2.3.折线

使用AMap.Polyline构建函数创建实例--->调用map的add方法将实例添加到地图上

2.4.多边形

2.4.1创建多边形

在地图上点击按钮给地图绑定点击事件--->根据事件对象获取点标记的经纬度--->组成一个数组传入AMap.Polygon构造函数--->调用add方法将生成的实例渲染到地图上

或者

直接使用AMap.MouseTool鼠标绘制工具类 建议使用第二种方法,用户体验比较好

2.5.圆

2.5.1.创建圆

使用AMap.Circle(opt:CircleOptions)构建函数创建实例--->调用map的add方法将实例添加到地图上

2.5.2.编辑圆

首先根据后端返回的圆心经纬度和半径回显圆--->然后使用AMap.CircleEditor(Map,Circle)创建实例--->调用open方法开启编辑--->监听move、adjust事件

2.5.3.根据id唯一标识查看(放大)圆

遍历后端返回的圆形覆盖物列表数据(array),将唯一标识id绑定到AMap.Circle的extData参数上面,这样点击表格数据的每一行就可以根据这个唯一标识结合Circle类的getExtData()方法找到这个圆形覆盖物,然后调用map.setFitView()方法将该圆形覆盖物放大到合适的视野级别

2.5.4.判断圆和圆的位置关系

这个高德好像没有提供相关方法,自己实现,思路也很简单,大致思路就是判断圆心距离和半径之和的大小关系。怎么计算圆心距离可以参考api文档里面的【参考手册 数学计算库】

    doesCircleIntersect() {
      const circlesData = this.allCircleData.filter(item => item.type !== 3)
      if (!circlesData.length) {
        return
      }
      const isIntersect = circlesData.some(item => {
        const marker1 = new window.AMap.LngLat(item.lon, item.lat)
        const marker2 = new window.AMap.LngLat(this.circleOptions.lon, this.circleOptions.lat)
        const circleCenterDistance = marker1.distance(marker2)
        const circleRadiusSum = item.radius + this.circleOptions.radius
        return circleCenterDistance < circleRadiusSum
      })
      return isIntersect
    },

2.5.5.判断圆和点标记的位置关系

直接调用Circle类的contains(point:LngLat)方法,这个方法返回一个boolean,用于判断圆形覆盖物是否包含点标记

2.6.信息窗体

2.6.1.打开信息窗体

    // 方法一:html字符串拼接思路实现
    // 打开信息窗体
    openInfoWindow(info) {
      var infoList = ['<div class="info-container">']
      infoList.push(`<div class="info-container__header">飞手信息</div>`)
      infoList.push(`<div class="info-container__body">`)
      infoList.push(`<div>飞手姓名:${info.pilotName}</div>`)
      infoList.push(`<div>身份证号:${info.identityCard}</div>`)
      infoList.push(`<div>飞行时长:${info.duration}h</div>`)
      infoList.push(`<div>地址:${info.address}</div>`)
      infoList.push(`<div>经度:${info.pilotLon} 纬度:${info.pilotLat}</div>`)
      infoList.push(`</div></div>`)
      this.infoWindow = new window.AMap.InfoWindow({
        content: infoList.join(''), // 使用默认信息窗体框样式,显示信息内容
        offset: new window.AMap.Pixel(-5, -35)
      })
      this.infoWindow.open(
        this.map,
        new window.AMap.LngLat(info.pilotLon, info.pilotLat)
      )
    }
    
    // 方法二:使用组件的思路去实现,设置isCustom属性为true就可以随心所欲的去用组件去实现一个自定义的信息窗体
    // 打开信息窗体
    openInfoWindow(info) {
      this.showInfoWindow = true
      this.infoWindowContent = info
      const infoWindow = new window.AMap.InfoWindow({
        isCustom: true, // isCustom设置true 自定义信息窗体
        content: this.$refs.infoWindowRef.$el, // 指向自定义信息窗体dom元素
        offset: new window.AMap.Pixel(0, 0)
      })
      this.infoWindow = infoWindow
      this.infoWindow.open(this.map, new window.AMap.LngLat(info.lon, info.lat))
    }

以上两种方法都可以实现,方法一适用于展示内容比较简单的信息窗体,方法二相对而言更加灵活可扩展,写起来也很爽,也好维护,后期迭代只需要改动子组件即可

2.7.覆盖物群组

如果需要一次性给地图添加很多覆盖物,其中包括点标记、圆形、多边形等等,可以考虑使用AMap.OverlayGroup,比较简单粗暴

2.8.LBS服务

2.8.1.地理编码(地址->经纬度)

    /**
     * 根据关键词查询经纬度
     */
    getLngLatByKeywords(keywords) {
      const geocoder = new window.AMap.Geocoder()
      geocoder.getLocation(keywords, (status, result) => {
        if (status === 'complete' && result.geocodes.length) {
          const center = result.geocodes[0].location
          this.map.setCenter(center)
          this.map.setZoom(18)
        } else {
          this.$message.info('根据地址查询位置失败,请输入详细地址搜索')
        }
      })
    },

2.8.2.逆地理编码(经纬度->地址)

 /**
     * 根据经纬度查询位置
     */
    getAddressByLngLat(lnglat) {
      const geocoder = new window.AMap.Geocoder({
        city: '全国'
      })
      geocoder.getAddress(lnglat, (status, result) => {
        if (status === 'complete' && result.regeocode) {
          const address = result.regeocode.formattedAddress
          this.form.remarks = `以${address}为中心,以${this.circleOptions.radius}米为半径的圆形区域`
        } else {
          this.$message.info('根据经纬度查询地址失败')
        }
      })
    },

2.8.3.行政区划查询

这里就不详细说,分两种,行政区边界查询和下属行政区查询,一般会结合el-cascader级联选择器做一些行政区划查询的业务需求。

3.怎么封装业务组件

封装组件也很简单,不外乎组件通信,slot,mixins这些东西,当然,想要封装一个高可用、低耦合的组件还是比较考验人的功底,我一般是首先实现业务需求,然后再将很多页面复用的data、methods单独抽离出来,放到mixins里面,前提是这些data、methods具备单独抽离出来的条件,这个条件怎么界定的其实也没有一个标准,就看到底是在页面写还是在mixins里面写比较优雅。当然也可以直接封装一个单独的业务组件,比如信息窗体自定义组件,电子围栏组件等等

4.那些年Tony爬过的坑

什么...Tony???没错,不要怀疑,我以前不是理发的,只是英语不好所以取了这个简单又好记的名字,我很喜欢,毕竟已经伴随我好几年的职业生涯了

4.1.绘制多边形使用第一种方法点标记不按套路出牌,不按顺时针连接,也不按逆时针连接,却是交叉连接。这么说可能还是有些懵逼,我手绘几张图对比看一下就明白了

26eff89fd312073ea1898221bd3fdd7.png 期望的连接顺序是1-2-3-4-1 或者 1-4-3-2-1 然而 他偏偏不给面子逆天而行 1-2-4-3-1 或者 1-3-4-2-1

解决办法:先将经纬度坐标转换成平面坐标,根据平面坐标对点标记按照一定的规则排序,排好序再创建多边形,规则如下

    // start表示中心点  end表示多边形顶点
    getAngle (start, end) {
      // 首先将经纬度坐标转换为平面坐标
      const p_start = this.map.lngLatToContainer(start)
      const p_end = this.map.lngLatToContainer(end)
      const diff_x = p_end.x - p_start.x
      const diff_y = p_end.y - p_start.y
      return (360 * Math.atan2(diff_y, diff_x)) / (2 * Math.PI) + 180
    }
    
    // 遍历顶点集合,调用getAngle方法获取每个顶点和中心点成的角度(<90°),根据角度对顶点进行排序,排好序再绘制多边形即可,后续代码省略

4.2.列表页新增圆形(多边形)覆盖物成功之后,立即点击编辑这条新增记录,打开el-dialog对话框,对话框里面的地图会出现首次渲染失败的神奇bug

解决办法:新增页面和编辑页面地图容器id名取名区别开

结语

行文至此,想聊的基本都聊了,可能还有一些高级用法,不常见的bug我还没接触过,算不上干货满满,也算不上废话连篇,只能说是给新人一点小小的指引,希望看了我的文章可以少走一点弯路,有什么想法也可以留言相互交流。