高德地图绘制轨迹+点聚合+描点

1,410 阅读2分钟

前言

最近实现了一个业务需求:根据船讯网给的数据,绘制船舶的行进轨迹。 进行轨迹的绘制,百度地图(vue-baidu-map)和高德地图(vue-amap)都提供了可用的API。起初选择了百度地图,但是无法很好的解决跨180°经线衔接的问题,进行分段绘制后,轨迹仍然不能够衔接起来,所以最后选择了高德地图进行了实现。

实现思想

实现中需要解决的两个关键问题是:

  1. 轨迹跨180°经线衔接问题;
  2. 海量点进行描点处理严重卡顿问题

针对这两个问题的解决方案:

  1. 跨180°经线问题:分段绘制。从180°经线处插入分割点,然后根据分割点,分东西半球进行绘制(注意:百度地图使用此方案后,东西半球两段轨迹线不能连接在一起,[-180°, 0°]和[0°, 180°]这两个范围的轨迹线在视图中不能共存),代码如下:
      const list = [ .... ]    // 轨迹点坐标
      const pois = []          // 存储坐标点(包含分割点)
      const splitIndex = [0]   // 标记分隔位置
      
      list.map((item, index) => {
        // 将坐标转换为高德坐标
        const curLngLat = new AMap.LngLat(item.lon, item.lat)
        pois.push(curLngLat)
        // 跨过180°经线插入分割点
        if (index + 1 < list.length) {
          if (item.lon > 0 && list[index + 1].lon < 0 && item.lon > 170) {
            pois.push(new AMap.LngLat(180, item.lat))
            pois.push(new AMap.LngLat(-180, item.lat))
            splitIndex.push(index + 2)
          }
          if (item.lon < 0 && list[index + 1].lon > 0 && list[index + 1].lon > 170) {
            pois.push(new AMap.LngLat(-180, item.lat))
            pois.push(new AMap.LngLat(180, item.lat))
            splitIndex.push(index + 2)
          }
        }
      })

      // 分段绘制轨迹
      splitIndex.forEach((item, index) => {
        const polyline = new AMap.Polyline({
          path: pois.slice(item, splitIndex[index + 1]),
          showDir: true,
          strokeWeight: 8,         // 折线的宽度,以像素为单位
          strokeOpacity: 0.8,      // 折线的透明度,取值范围0 - 1
          strokeColor: '#FB991C',  // 折线颜色
          lineJoin: 'round'
        })
        map.add(polyline)
      })
  1. 卡顿问题:海量点聚合。高德API中提供了海量点聚合的处理方式,有效避免了加载大于3000个点时出现卡顿情况,并且能够根据地图级别来显示轨迹上的点。
      const markerPoints = []   // 轨迹上的点

      list.map((item, index) => {
        // 将坐标转换为高德坐标
        const curLngLat = new AMap.LngLat(item.lon, item.lat)
        
        // 创建marker
        const marker = new AMap.Marker({
          position: curLngLat,
          icon: require('@/assets/images/ship.png'),
          info: item.timestampStr,
          size: new AMap.Size(64, 72),
          zIndex: 500
        })
        markerPoints.push(marker)
        // 显示节点信息
        _this.handleInfoShow(marker, item.timestampStr)
      })
      
      // 定义轨迹上的点的样式
      const _renderClusterMarker = context => {
        // 显示聚合点中第一个点的位置
        context.marker.setContent('<img src="' + require('@/assets/images/ship.png') + '" width="64" height="64">')
        // 显示节点信息
        _this.handleInfoShow(context.marker, context.markers[0].De.info)
      }

      // 聚合点,绘制轨迹上的点
      AMap.plugin(['AMap.MarkerClusterer'], () => {
        new AMap.MarkerClusterer(map, markerPoints, {
          gridSize: 80, 
          averageCenter: false, // 显示聚合点中第一个点的位置
          renderClusterMarker: _renderClusterMarker // 自定义聚合点的样式
        })
      })

具体实现

  1. 安装依赖
npm install vue-amap --save
  1. 申请API key,在项目中注册
    AMapLoader.load({
      key: '你申请的key',
      plugins: ['AMap.Scale', 'AMap.OverView', 'AMap.ToolBar', 'AMap.PolyEditor'] // 需要使用的的插件列表
    }).then(AMap => {
      this.initMap(AMap) // 绘制轨迹
    })
  1. 完整代码
<template>
  <div>
    <div id="map-container" />
  </div>
</template>

<script>
import AMapLoader from '@amap/amap-jsapi-loader'

export default {
  name: 'Map',
  props: {
    mapInfo: { type: Object, default: null }
  },
  data() {
    return {
      AMap: null,
      center: { lon: 160.2222222, lat: 20.222222 },
      zoom: 3,
      pointList: [
        // { lon: 160.2222222, lat: 21.222222 },
        // { lon: 162.2222222, lat: 22.222222 },
        // { lon: 165.2222222, lat: 23.222222 },
        // { lon: 168.2222222, lat: 24.222222 },
        // { lon: 170.2222222, lat: 23.332222 },
        // { lon: 175.2222222, lat: 22.222222 },
        // { lon: 177.2222222, lat: 22.002222 },
        // { lon: 178.2222222, lat: 21.222222 },
        // { lon: -179.2222222, lat: 20.222222 },
        // { lon: -178.2222222, lat: 19.222222 },
        // { lon: -170.2222222, lat: 18.222222 },
        // { lon: -165.2222222, lat: 19.222222 },
        // { lon: -160.2222222, lat: 20.222222 }
      ]
    }
  },
  mounted() {
    // 加载地图
    AMapLoader.load({
      key: 'c3a30011fff38e674d7348bc416faecb', // 申请好的Web端开发者Key,首次调用 load 时必填
      plugins: [
        'AMap.Scale', 
        'AMap.OverView', 
        'AMap.ToolBar',
        'AMap.PolyEditor'
      ]    // 需要使用的的插件列表
    }).then(AMap => {
      this.AMap = AMap
      this.initMap(AMap)   // 绘制轨迹
    })
  },
  methods: {
    initMap(AMap) {
      const _this = this
      if (_this.pointList?.length > 0) {
        _this.center = _this.mapInfo.pointList[0]
      }
      const map = new AMap.Map('map-container', {
        resizeEnable: true, 
        center: new AMap.LngLat(_this.center.lon, _this.center.lat), // 初始化地图中心点位置
        zoom: _this.zoom                                             // 初始化地图级别
      })
      
      map.addControl(new AMap.ToolBar())                             // 开启工具条
      map.addControl(new AMap.Scale())                               // 开启比例尺
      map.clearMap()                                                 // 清除覆盖物

      // 没有数据点,不进行绘制
      if (_this.pointList?.length === 0) {
        return
      }

      const list = _this.pointList            // 轨迹点
      const pois = []                         // 存储坐标点(包含分割点)
      const splitIndex = [0]                  // 标记分隔位置
      const markerPoints = []                 // 轨迹上的点

      list.map((item, index) => {
        // 将坐标转换为高德坐标
        const curLngLat = new AMap.LngLat(item.lon, item.lat)
        pois.push(curLngLat)
        // 跨过180°经线插入分割点
        if (index + 1 < list.length) {
          if (item.lon > 0 && list[index + 1].lon < 0 && item.lon > 170) {
            pois.push(new AMap.LngLat(180, item.lat))
            pois.push(new AMap.LngLat(-180, item.lat))
            splitIndex.push(index + 2)
          }
          if (item.lon < 0 && list[index + 1].lon > 0 && list[index + 1].lon > 170) {
            pois.push(new AMap.LngLat(-180, item.lat))
            pois.push(new AMap.LngLat(180, item.lat))
            splitIndex.push(index + 2)
          }
        }

        // 生成marker, 放入markerPoints
        // 起点和终点,不放入轨迹上的点,直接显示
        if (index === 0 || index === list.length - 1) {
          return
        }
        // 创建marker
        const marker = new AMap.Marker({
          position: curLngLat,
          icon: require('@/assets/images/ship.png'),
          info: item.timestampStr,
          size: new AMap.Size(64, 72),
          zIndex: 500
        })
        markerPoints.push(marker)
        // 显示节点信息
        _this.handleInfoShow(marker, item.timestampStr)
      })

      // 分段绘制轨迹
      splitIndex.forEach((item, index) => {
        const polyline = new AMap.Polyline({
          path: pois.slice(item, splitIndex[index + 1]),
          showDir: true,
          strokeWeight: 8, // 折线的宽度,以像素为单位
          strokeOpacity: 0.8, // 折线的透明度,取值范围0 - 1
          strokeColor: '#FB991C', // 折线颜色
          lineJoin: 'round'
        })
        map.add(polyline)
      })

      // 定义轨迹上的点的样式
      const _renderClusterMarker = context => {
        // 显示聚合点中第一个点的位置
        context.marker.setContent('<img src="' + require('@/assets/images/ship.png') + '" width="64" height="64">')
        // 显示节点信息
        _this.handleInfoShow(context.marker, context.markers[0].De.info)
      }

      // 聚合点,绘制轨迹上的点
      AMap.plugin(['AMap.MarkerClusterer'], () => {
        new AMap.MarkerClusterer(map, markerPoints, {
          gridSize: 80, 
          averageCenter: false, // 显示聚合点中第一个点的位置
          renderClusterMarker: _renderClusterMarker // 自定义聚合点的样式
        })
      })

      // 添加起点描述
      const start = list[0]
      const startMark = _this.createMarker(start, require('@/assets/images/start.png'), '起运港', _this.mapInfo.departurePort, start.timestampStr)
      map.add(startMark)
      // 添加终点描述
      const end = list[list.length - 1]
      const endMark = _this.createMarker(end, require('@/assets/images/ship.png'), '目的港', _this.mapInfo.arrivalPort, end.timestampStr)
      map.add(endMark)
    },
    // 创建marker
    createMarker(point, iconUrl, title, tValue, time) {
      const marker = new this.AMap.Marker({
        position: new this.AMap.LngLat(point.lon, point.lat),
        icon: iconUrl,
        label: {
          direction: 'top',
          content: `
            <div style="padding:5px;font-size:12px;">
              <div style="padding-bottom:5px">${title || ''}${tValue || ''}</div>
              <div>时&nbsp;&nbsp;&nbsp;间:${time || ''}</div>
            </div>
          `
        },
        zIndex: 1000
      })
      return marker
    },
    // 信息显示与隐藏
    handleInfoShow(marker, info) {
      // 鼠标滑过,显示信息
      marker.on('mouseover', e => {
        e.target.setLabel({
          direction: 'top',
          content: `<div>${info || ''}</div>`
        })
      })

      // 鼠标移出,隐藏信息
      marker.on('mouseout', e => {
        e.target.setLabel(null)
      })
    }
  }
}
</script>

<style lang="scss">
#map-container {
  width: 100%;
  height: 370px;
}
.amap-marker-label{
  border: none;
  border-radius: 3px;
  padding:  8px;
  box-shadow: 0 2px 6px 0 rgba(114, 124, 245, .5);
  background-color: #fff;
  text-align: left;
  font-size: 12px;
}
.amap-logo{
  display: none;
  opacity:0 !important;
}
.amap-copyright {
  opacity:0;
}
</style>

  1. 实现效果