高德地图-散点地图-实现在地图上绘制散点并绑定交互事件

1,069 阅读8分钟

哈喽,各位高贵的倔友们你们好呀(✪ω✪),今天继续分享关于高德地图内容的文章,在观看此篇文章之前,你也可以先看看上一篇文章,这样能更好理解也能学到更多东西唷。那么,如果各位看官大大觉得还不错,期望点个赞呗,我们话不多说,直接进入正题。

写在开头

高德地图的申请、安装与初始化我们在上一篇文章讲过,这里就不多说啦,直接来进入本章的主题内容。

0129FB34.gif

数据准备

这里小编先准备了一些测试数据,下面会以这种数据结构来编写逻辑,如果你真实的数据结构与此不同,可以自行再转换一下或者适当调整一下代码逻辑。

创建 data.js 文件:

const data = [
  {
    "province": "北京市",
    "city": "北京城区",
    "area": "西城区",
    "append": "北京市北京城区西城区3",
    "lng": "116.365868",
    "lat": "39.912289",
  },
  {
    "province": "上海市",
    "city": "上海城区",
    "area": "静安区",
    "append": "上海市上海城区静安区5",
    "lng": "121.459384",
    "lat": "31.247105",
  },
  {
    "province": "广东省",
    "city": "广州市",
    "area": "黄埔区",
    "append": "广东省广州市黄埔区4",
    "lng": "113.459749",
    "lat": "23.106402",
  },
  {
    "province": "四川省",
    "city": "成都市",
    "area": "青羊区",
    "append": "四川省成都市青羊区6",
    "lng": "104.062499",
    "lat": "30.674406",
  },
  {
    "province": "河北省",
    "city": "石家庄市",
    "area": "桥西区",
    "append": "河北省石家庄市桥西区2",
    "lng": "114.461154",
    "lat": "38.004043",
  },
  {
    "province": "重庆市",
    "city": "重庆城区",
    "area": "涪陵区",
    "append": "重庆市重庆城区涪陵区1",
    "lng": "107.389298",
    "lat": "29.703113",
  },
];

export default data;

绘制散点

在地图上绘制散点标记,这次小编使用的是高德地图提供的 AMap.CircleMarker 对象。

image.png

它能通过经纬度坐标帮我们在地图上快速绘制出一个点形状的圆形,非常的方便,你可以先自己瞅瞅官方的示例,传送门

当然,高德地图官方提供了很多与散点绘制相关的 API,这里小编之所以选择这个 API 对象,主要出于以下三点考虑衡量:

  1. 支持自定义样式,比如每个散点的背景色、边框颜色、散点大小等。
  2. 支持多种交互事件绑定,比如点击事件、双击事件、鼠标移入移出事件等。
  3. 通过经纬度坐标绘制标记点,由于小编的业务中,每条数据是携带有各自的经纬度坐标的,所以使用起来比较方便,不过,下面小编也会兼容没有经纬度坐标的数据,没有就让它们用地址信息去高德那边查。

接下来,我们来看看具体的绘制过程:

<template>
  <div id="container" />
</template>

<script>
import AMapLoader from '@amap/amap-jsapi-loader';
import data from './data.js';

export default {
  data() {
    return {
      // map:null,
    }
  },
  async mounted() {
    await this.initMap();
    this.drawScatterMap();
  },
  methods: {
    /** @name 初始化地图 **/
    initMap() {
      return new Promise(resolve => {
        window._AMapSecurityConfig = {
          securityJsCode: '你的安全密钥',
        }
        AMapLoader.load({
          key: '你的Key',
          version: '2.0',
          plugins: [],
        }).then((AMap)=>{
          this.map = new AMap.Map('container', {
            center: [110.17293, 35.20173],
            zoom: 3.8,
        });
        this.map.on('complete', () => {
           resolve();
        });
        }).catch(e=>{
           console.log(e);
        })
      })
    },
    /** @name 获取随机颜色 **/
    getRandomColor() {
      let color = '#';
      for(let i = 0; i < 6; i++){
        color += (Math.random() * 16 | 0).toString(16);
      }
      return color;
    },
    /** @name 绘制散点地图 **/
    drawScatterMap() {
      for (const [index, address] of data.entries()) {
        let center = [address.lng, address.lat];
        if (center[0] && center[1]) {
          const color = this.getRandomColor();
          const scatterInstance = new window.AMap.CircleMarker({
            center, // 经纬度坐标
            radius: 5, // 散点半径: 0~64
            fillColor: color, // 填充颜色, 仅支持16进制颜色值, 默认值为#00D3FC
            fillOpacity: 0.8, // 填充颜色透明度
            strokeColor: color, // 边框颜色
            strokeWeight: 1, // 边框大小
          });
          scatterInstance.setMap(this.map);
        }
      }
    },
  }
}
</script>

<style scoped>
#container {
  width: 740px;
  height: 500px;
}
</style>

具体效果:

image.png

查询经纬度

经纬度坐标是在地图上标记的前提,但对于一些只有地址信息没有经纬度坐标的数据,高德地图官方也提供了相关的转换 API,它主要就是 AMap.Geocoder 对象,该对象支持用于地址信息描述与经纬度坐标之间的转换。

来看看使用示例:

const geocoder = new window.AMap.Geocoder();
geocoder.getLocation('广东省广州市白云区白云山', (status, result) => {
  console.log(status, result)
});

image.png

需要注意,每个平台地图所拥有的经纬度坐标体系是不同的,所产生出来的经纬度坐标也是有差异的,例如你用百度地图的经纬度坐标来高德地图上绘制标记点,实际可能会产生一些偏差,这点是需要我们注意的!

那么,我们把它加入到绘制散点的编码中,具体如下:

<script>
import AMapLoader from '@amap/amap-jsapi-loader';
import data from './data.js';

let geocoder = null;

export default {
  ...
  methods: {
    ...,
    /** @name 初始化地图 **/
    initMap() {
      return new Promise(resolve => {
        ...
        AMapLoader.load({
          key: '你的Key',
          version: '2.0',
          plugins: [
            'AMap.Geocoder', // 配置额外的插件
          ],
        }).then((AMap)=>{
        ...
      })
    },
    /** @name 根据地址从高德获取地址的经纬度信息 **/
    geoCode(address) {
      return new Promise(resolve => {
        if (!geocoder) {
          geocoder = new window.AMap.Geocoder();
        }
        geocoder.getLocation(address, (status, result) => {
          if (status === 'complete' && result.geocodes.length) {
            const lnglat = result.geocodes[0].location;
            resolve(lnglat);
          } else {
            console.warn('根据地址查询位置失败:' + address);
          }
        });
      });
    },
    /** @name 绘制散点地图 **/
    drawScatterMap() {
      for (const [index, address] of data.entries()) {
        let center = [address.lng, address.lat];
        // 兼容缺失经纬度坐标的数据
        if (!center[0] || !center[1]) {
          // 前端通过高德API自行查经纬度
          const result = await this.geoCode(address.append);
          center = [result.lng, result.lat];
          // 如果还是查不到经纬度,就放弃这个点
          if (!center[0] || !center[1]) continue;
        }
        if (center[0] && center[1]) {
          const color = this.getRandomColor();
          const scatterInstance = new window.AMap.CircleMarker({
            center,
            radius: 5,
            fillColor: color,
            fillOpacity: 0.8,
            strokeColor: color,
            strokeWeight: 1,
          });
          scatterInstance.setMap(this.map);
        }
      }
    },
  }
}
</script>

你可以尝试把数据中的经纬度坐标删除进行测试。如果你的地址信息越详情,散点标记的位置就越准确,如果缺失除省级中的"任何一级"的信息,会直接往省会城市标记。

注意,由于“地址信息转经纬度坐标”这个过程本质也是发送请求到高德那边去查,如果数据量很大的话,而且数据都要转换,那么这个转换过程需要大量的时间。

这里小编建议,如果是海量数据标点,可以让后端写个接口先把数据挨个发去高德那边查,再把查到的经纬度坐标同步到数据库中,这样子能避免前端渲染性能问题。

当然,最好的方式就是在用户产生地址信息数据的时候,就顺便存储经纬度坐标,这是最好的了(✪ω✪)。

手动控制散点大小

接下来,我们继续来完成一个“控制散点大小”的功能,本质就是拿 radius 半径字段做文章,高德地图官方对于 radius 的限制范围是:0~64

其实在实际业务中,散点的大小很多时候都表示一条数据相比于另一条数据的大小,这个"大小"它可以是一条数据中的某个字段比较,也可以是某几个字段之和比较。但是,最终每条数据都会有产生一个值,用来控制这个半径的大小,而这个值往往并不能直接赋值给到半径,它需要经过相关的公式转换

下面我们先来看一个效果:

5.gif

小编通过一个进度条来控制散点的大小,这个进度条的最小值为 0,最大值为 100,而且小编期望散点的自身最小值限制为 5,这样不至于整个点都消失了。

那么应该如何来完成这个转换过程呢?公式如下:

半径大小 = 最小值 + (最大值 - 最小值) * (当前值 / 100)

具体编码过程:

<template>
  <div class="map">
    <div id="container" />
    <div>
     <input v-model="size" @input="sizeChange" type="range" min="0" max="100" />
     {{ size }}
    </div>
  </div>
</template>

<script>
import AMapLoader from '@amap/amap-jsapi-loader';
import data from './data.js';

let geocoder = null;
let scatterInstanceAll = [];

export default {
  data() {
    return {
      // map:null,
      size: 0,
    }
  },
  ...,
  methods: {
    ...,	
    /** @name 散点大小 **/
    sizeChange() {
      this.drawScatterMap();
    },
    /** @name 绘制散点地图 **/
    async drawScatterMap() {
      // 清空,否则会重复标记
      if (scatterInstanceAll.length > 0) {
        this.map.remove(scatterInstanceAll);
        scatterInstanceAll = [];
      }
      for (const [index, address] of data.entries()) {
        let center = [address.lng, address.lat];
        if (!center[0] || !center[1]) {
          const result = await this.geoCode(address.append);
          center = [result.lng, result.lat];
          if (!center[0] || !center[1]) continue;
        }
        if (center[0] && center[1]) {
          // 计算半径大小
          let radius = 5 + (64 - 5) * (this.size / 100);
          const color = this.getRandomColor();
          const scatterInstance = new window.AMap.CircleMarker({
            center,
            radius,
            fillColor: color,
            fillOpacity: 0.8,
            strokeColor: color,
            strokeWeight: 1,
          });
          scatterInstance.setMap(this.map);
          scatterInstanceAll.push(scatterInstance);
        }
      }
    },
  }
}
</script>

<style scoped>
.map {
  width: 740px;
  height: 500px;
  position: relative;
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
}
#container {
  width: 100%;
  height: 100%;
  margin-right: 10px;
}
</style>

Em...在小编实际的业务中,这个半径的控制其实考虑得挺复杂的,小脑瓜想了半天就想到这么一个小例子来表达所想,感觉还是欠缺一点意思,但是抓住核心就是 转换

01F8CB8A.png

绑定交互事件

高德地图官方文档中,对于每个"对象"所能绑定的事件都有明确的列举。

image.png

但是,小编想说,文档中好像也不全面,像有些对象文档中并没有写支持点击事件,但实际添加监听,还是可以触发,这....很迷(✪ω✪)。

反正就那样吧,我们继续来看看如何给每个散点绑定事件,直接来看代码:

export default {
  ...,
  methods: {
    ...,	
    /** @name 绘制散点地图 **/
    async drawScatterMap() {
      ...
      for (const [index, address] of data.entries()) {
        ...
        if (center[0] && center[1]) {
          let radius = 5 + (64 - 5) * (this.size / 100);
          const color = this.getRandomColor();
          const scatterInstance = new window.AMap.CircleMarker({
            center,
            radius,
            fillColor: color,
            fillOpacity: 0.8,
            strokeColor: color,
            strokeWeight: 1,
            zIndex: index, // 层级: 层级越高越靠上方
            cursor: 'pointer', // 指定鼠标悬停时的鼠标样式
            clickable: true, // 点标记是否可点击
            extData: { // 额外自定义属性
              index,
              address,
            },
          });
          scatterInstance.setMap(this.map);
          scatterInstanceAll.push(scatterInstance);
          // 绑定交互事件
          scatterInstance.on('click', e => {
            const extData = e.target._opts.extData;
            console.log(extData);
          });
        }
      }
    },
  }
}
</script>

我们随便点击两个散点,可以获取到绘制时额外携带的属性,这就能方便我们扩展后续的一系列功能了。

image.png

你以为到这里就完了?当然还没(✪ω✪),我们再来实现一个小功能,当两个散点叠加再一起的时候,我们期望鼠标移入到哪个点身上,这个点层级能在最上方,移除的时候又恢复成原样。

效果如下:

7.gif

完成这个功能我们需要绑定 mouseovermouseout 事件,具体编码过程:

export default {
  ...,
  methods: {
    ...,	
    /** @name 绘制散点地图 **/
    async drawScatterMap() {
      ...
      for (const [index, address] of data.entries()) {
        ...
        if (center[0] && center[1]) {
          let radius = 5 + (64 - 5) * (this.size / 100);
          const color = this.getRandomColor();
          const scatterInstance = new window.AMap.CircleMarker({
            center,
            radius,
            fillColor: color,
            fillOpacity: 0.8,
            strokeColor: color,
            strokeWeight: 1,
            zIndex: index, // 层级: 层级越高越靠上方
            cursor: 'pointer',
            clickable: true,
            extData: {
              index, // 把层级携带下去
              address,
            },
          });
          scatterInstance.setMap(this.map);
          scatterInstanceAll.push(scatterInstance);
          // 绑定交互事件
          scatterInstance.on('click', e => {});
          scatterInstance.on('mouseover', (e) => {
            const extData = e.target._opts.extData;
            // 散点层级变大
            scatterInstanceAll[extData.index].setOptions({
              zIndex: data.length + 10,
            });
          });
          scatterInstance.on('mouseout', (e) => {
            const extData = e.target._opts.extData;
            // 散点层级恢复
            scatterInstanceAll[extData.index].setOptions({
              zIndex: extData.index,
            });
          });
        }
      }
    },
  }
}
</script>

不过,这些都是比较简单的操作,相信难不倒各位优秀的倔友(✪ω✪),那么小编给各位铁汁们准备了一个小作业,完成以下这么一个功能:

  1. 鼠标移动到某个散点,能展示出一个"详情列表"。
  2. 鼠标快速移入移出某个散点、或者两个散点之间移动切换,如何更好的展示该浮层列表?
  3. 实现移入散点展示,移出散点关闭,移入浮层展示,移出浮层关闭。
  4. 因为可能存在多条数据标记在同一个点,所以同一个点可能会有多条数据,故该浮层列表需要做分页处理,这又该如何处理?
  5. 实现浮层列表的数据可点击。

image.png

就这样子,应该不难,感兴趣的小伙伴们可以试试,加油加油。

0055AA5B.gif

完整源码

点我点我





至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。