高德地图上来个”画圈为牢“——贝壳画圈找房

896 阅读1分钟

不知道大家有没有玩过一个“游戏”,就是在地上发现蚂蚁时,可以围绕它,在它周围画一个圈啥的,蚂蚁 🐜 就被圈起来了,出不去了,有种画地为牢的感觉。看过西游记的应该也记得,猴哥出门时总是会给师傅画个圈,也算是画地为牢...

本想进入正题,但是作为圈内人士,受圈内文化熏陶,不得不在此卷一下,凡事问个原理,为什么?

画地为牢 解析

如果没有玩过蚂蚁那个游戏的,可以看看 画地为牢的蚂蚁视频

进入正题吧,自身需求来源就是老板想在地图上画个圈,看看这个圈内范围的都有哪些标记点,这...

其实之前也看到过该类似效果,如链家找房的画圈找房功能,大家可以体验一下!确实做的呢不错。

那就简单的做个demo,先实现一下大概功能:

1、先来个地图吧

由于公司内部用的高德地图居多,所以我们就用高德地图来做这个demo,高德开放平台,记得申请自己的 key 啊!

用 vue2 吧,毕竟该需求还是在 vue2 项目里边做,其他vue3,React 框架用法参考链接,或者开源社区。

准备好的我的页面布局:

<template>
  <div class="map-page">
    <div id="container" class="map-container" />
    <div class="btn-box">
      <button v-if="!drawing" class="btn" @click="drawClick">画圈找点</button>
      <button v-else class="btn" @click="noDrawClick">退出画圈</button>
    </div>
    <ul v-if="selectedArr.length>0" class="selectedArr">
      <li v-for="item in selectedArr" :key="item[0]">
        {{ `${item[2]}:坐标为 [${item[0]},${item[1]}]` }}
      </li >
    </ul >
  </div>
</template>
<style scoped>
.map-page {
  height: 100%;
}
.map-container {
  height: 100%;
  width: 100%;
}
.btn-box {
  position: fixed;
  top: 50px;
  right: 80px;
  padding: 12px 15px;
  background: #fff;
  box-shadow: 0 0 8px 0 rgb(0 0 0 / 8%);
  border-radius: 4px;
  font-family: PingFangSC-Medium;
  font-size: 14px;
  color: #333;
  cursor: pointer;
  display: flex;
  justify-content: space-around;
}
.btn-box .btn {
  outline: none;
  border: none;
  font-size: 17px;
  color: #fff;
  border-radius: 4px;
  padding: 8px;
  background-color: #909399;
  cursor: pointer;
  transition: all 0.5s;
}
.btn-box .btn:hover {
  background-color: #409EFF;
}
.selectedArr{
  position: fixed;
  top: 150px;
  right: 80px;
  background: #fff;
  padding: 20px 20px 10px;
}
.selectedArr li{
  padding-bottom: 10px;
}
</style>

接下来,整活:

安装地图:

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

初始化地图:

import AMapLoader from '@amap/amap-jsapi-loader';

data(){ 
    return{
        AMap: null,
        map:null, 
    } 
},
mounted(){ 
    //DOM初始化完成进行地图初始化 
    this.initMap(); 
}
methods:{
    initMap(){
      AMapLoader.load({
        key:"",             // 申请好的Web端开发者Key,首次调用 load 时必填
        version:"2.0",      // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
        plugins:[''],       // 需要使用的的插件列表,如比例尺'AMap.Scale'等
      }).then((AMap)=>{
        this.map = new AMap.Map("container",{  //设置地图容器id
          viewMode:"3D",    //是否为3D地图模式
          zoom:5,           //初始化地图级别
          center:[105.602725,37.076636], //初始化地图中心点位置
        });
      }).catch(e=>{
        console.log(e);
      })
    },
},

基本上得到一个“光秃秃”的地图了:

WechatIMG5.png

2、地图上标记的业务坐标点吧

//
dataList: [
    [116.351951, 39.929543, '北京国宾酒店'],
    [116.404556, 39.92069, '故宫博物院'],
    [116.479008, 39.925781, '呼家楼'],
    [116.368624, 39.870869, '首都医科大学'],
    [116.4471, 39.849601, '宋家庄']
],
    
// 初始化点
initMapMarkers() {
  this.dataList.forEach((item) => {
    this.map.add(
      new this.AMap.Marker({
        position: [item[0], item[1]]
      })
    )
  })
},

3、给按钮绑定地图鼠标事件吧

// 是否处于画圈状态下
drawing: false,
      
// 开始画圈
drawClick() {
  this.map.setStatus({
    doubleClickZoom: false,
    scrollWheel: false,
    dragEnable: false,
    keyboardEnable: false
  })
  this.map.setDefaultCursor('crosshair')
  this.drawing = true
},
// 取消画圈
noDrawClick() {
  this.map.setStatus({
    doubleClickZoom: true,
    scrollWheel: true,
    dragEnable: true,
    keyboardEnable: true
  })
  this.map.setDefaultCursor('default')
  this.drawing = false
  this.selectedArr = []
  this.map.remove(this.lastPolyLine)
  this.map.remove(this.polygonAfterDraw)
}

4、实现地图上的画笔吧

脑暴开始...

如何实现这种类似画笔的画操作?我翻看了高德地图关于画图的所有api后,只发现高德地图提供了绘制圆,直线,多边形,矩形的api,官方示例

玩了半天,感觉其中最符合的就是画矩形或者圆,但是这样达不到那种随意连贯的效果,仔细研究了链家网画的圈,放大地图进行画图查看,发现看似很连贯画出的图在放大状态下是折线组成,这就说明了其实是用线段模拟画圈操作的。那折线api是 AMap.Polyline。

于是,逐渐有了思路。之前做过出行类的行程轨迹之类的功能,这就类似了。如果要是能获取这样一个由不同point组成的数组,然后调用该api就能在地图上画图。但是,如何获取数组?

其实,地图有自带的mousemove事件,获取到mousemove回调里的参数能够获取鼠标在地图上的坐标点。

既然点能获取到了,那么就用一个数组把这些点保存下来用于后续画线操作。然后整个画线逻辑就很明显了:每次mousemove触发都往数组中push当前鼠标所在点,然后调用api进行画线,同时用一个lastPolyLine变量记录下上次画的线,因为每次mousemove触发都会把从头到尾把整个数组的点画出来,所以需要擦除上次画的线段,然后画上新的线段,否则地图上的线段将会重叠起来越积越多。

继续分析,当鼠标抬起时表明画线完成,此时地图上会显示一个有填充颜色的多边形,这个怎么处理?也很简单,用高德地图提供的画多边形的api,AMap.Polygon。

其参数恰好也是一个由Point组成的数组,这个数组就是上述画线的数组。因此当鼠标抬起时,擦除上次画的线,然后再根据polyPointArray绘制一个多边形不就画出了整个圈了么!

脑暴结束...

小二,上代码!

// 是否处于画圈状态下
drawing: false,
// 是否处于鼠标左键按下状态下
isMouseDown: false,
// 存储画出折线点的数组
polyPointArray: [],
// 上次操作画出的折线
lastPolyLine: null,
// 画圈完成后生成的多边形
polygonAfterDraw: null
      

// 初始化鼠标画圈事件
initMapMouseHandle(AMap) {
  this.map.on('mousemove', (e) => {
    if (this.isMouseDown) {
      const point = [e.lnglat.lng, e.lnglat.lat]
      this.polyPointArray.push(point)
      if (this.lastPolyLine) {
        this.map.remove(this.lastPolyLine)
      }
      const polyline = new AMap.Polyline({
        path: this.polyPointArray,
        strokeColor: '#00ae66',
        strokeOpacity: 2
      })
      this.map.add(polyline)
      this.lastPolyLine = polyline
    }
  })
  this.map.on('mousedown', () => {
    if (this.drawing) {
      this.lastPolyLine && this.map.remove(this.lastPolyLine)
      this.lastPolyLine = null
      this.polyPointArray = []
      this.isMouseDown = true
    }
  })
  document.addEventListener('mouseup', () => {
    if (this.drawing && this.isMouseDown) {
      // 退出画线状态
      this.isMouseDown = false
      // 添加多边形覆盖物,设置为禁止点击
      this.polygonAfterDraw = new AMap.Polygon({
        path: this.polyPointArray,
        strokeColor: '#00ae66',
        strokeOpacity: 1,
        fillColor: '#00ae66',
        fillOpacity: 0.3
      })
      this.map.add(this.polygonAfterDraw)
    }
  })
},

5、收集圈中标记点吧

这块的难点就是判断我们的标记点那些在这个圈内,看看高德地图是否有相关api。

找啊找啊找“朋友”,找到一个“好朋友”---AMap.GeometryUtil.isPointInPolygon

selectedArr: []

// 收集圈内点的数据
isInPolygon() {
  this.dataList.forEach((item) => {
    if (
      this.AMap.GeometryUtil.isPointInPolygon(item, this.polyPointArray)
    ) {
      this.selectedArr.push(item)
    }
  })
},

最终的展示效果:

image.png

至此,一个简单的“画地为牢”的demo就出来了!有相关功能的需求,大家可以尝试一下,有好的建议也请留言,帮助改进!下边附上整体最终代码!

<template>
  <div class="map-page">
    <div id="container" class="map-container" />
    <div class="btn-box">
      <button v-if="!drawing" class="btn" @click="drawClick">画圈找点</button>
      <button v-else class="btn" @click="noDrawClick">退出画圈</button>
    </div>
    <ul v-if="selectedArr.length>0" class="selectedArr">
      <li v-for="item in selectedArr" :key="item[0]">
        {{ `${item[2]}:坐标为 [${item[0]},${item[1]}]` }}
      </li >
    </ul >
  </div>
</template>

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

export default {
  name: 'Map',
  data() {
    return {
      AMap: null, // 保存地图API,防止报错
      map: null,
      dataList: [
        [116.351951, 39.929543, '北京国宾酒店'],
        [116.404556, 39.92069, '故宫博物院'],
        [116.479008, 39.925781, '呼家楼'],
        [116.368624, 39.870869, '首都医科大学'],
        [116.4471, 39.849601, '宋家庄']
      ],
      // 是否处于画圈状态下
      drawing: false,
      // 是否处于鼠标左键按下状态下
      isMouseDown: false,
      // 存储画出折线点的数组
      polyPointArray: [],
      // 上次操作画出的折线
      lastPolyLine: null,
      // 画圈完成后生成的多边形
      polygonAfterDraw: null,
      selectedArr: []
    }
  },
  mounted() {
    this.initMap()
  },
  methods: {
    // 初始化地图数据
    initMap() {
      AMapLoader.load({
        key: '', // 申请好的Web端开发者Key,首次调用 load 时必填
        version: '2.0', // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
        plugins: [''] // 需要使用的的插件列表,如比例尺'AMap.Scale'等
      })
        .then((AMap) => {
          this.map = new AMap.Map('container', {
            // 设置地图容器id
            // viewMode: '3D', // 是否为3D地图模式
            zoom: 12, // 初始化地图级别
            center: [116.404, 39.915] // 初始化地图中心点位置
          })
          this.AMap = AMap
          //
          this.initMapMarkers(AMap)
          this.initMapMouseHandle(AMap)
        })
        .catch((e) => {
          console.log(e)
        })
    },
    // 初始化点
    initMapMarkers(AMap) {
      this.dataList.forEach((item) => {
        this.map.add(
          new AMap.Marker({
            position: [item[0], item[1]]
          })
        )
      })
    },
    // 初始化鼠标画圈事件
    initMapMouseHandle(AMap) {
      this.map.on('mousemove', (e) => {
        if (this.isMouseDown) {
          const point = [e.lnglat.lng, e.lnglat.lat]
          this.polyPointArray.push(point)
          if (this.lastPolyLine) {
            this.map.remove(this.lastPolyLine)
          }
          const polyline = new AMap.Polyline({
            path: this.polyPointArray,
            strokeColor: '#00ae66',
            strokeOpacity: 2
          })
          this.map.add(polyline)
          this.lastPolyLine = polyline
        }
      })
      this.map.on('mousedown', () => {
        if (this.drawing) {
          this.lastPolyLine && this.map.remove(this.lastPolyLine)
          this.lastPolyLine = null
          this.polyPointArray = []
          this.isMouseDown = true
        }
      })
      // todo 禁用scrollWheel属性map.on('mouseup')不生效?
      document.addEventListener('mouseup', () => {
        if (this.drawing && this.isMouseDown) {
          // 退出画线状态
          this.isMouseDown = false
          // 添加多边形覆盖物,设置为禁止点击
          this.polygonAfterDraw = new AMap.Polygon({
            path: this.polyPointArray,
            strokeColor: '#00ae66',
            strokeOpacity: 1,
            fillColor: '#00ae66',
            fillOpacity: 0.3
          })
          this.map.add(this.polygonAfterDraw)
          this.isInPolygon()
        }
      })
    },
    // 收集圈内点的数据
    isInPolygon() {
      this.dataList.forEach((item) => {
        if (
          this.AMap.GeometryUtil.isPointInPolygon(item, this.polyPointArray)
        ) {
          this.selectedArr.push(item)
        }
      })
    },
    // 开始画圈
    drawClick() {
      this.map.setStatus({
        doubleClickZoom: false,
        scrollWheel: false,
        dragEnable: false,
        keyboardEnable: false
      })
      this.map.setDefaultCursor('crosshair')
      this.drawing = true
    },
    // 取消画圈
    noDrawClick() {
      this.map.setStatus({
        doubleClickZoom: true,
        scrollWheel: true,
        dragEnable: true,
        keyboardEnable: true
      })
      this.map.setDefaultCursor('default')
      this.drawing = false
      this.selectedArr = []
      this.map.remove(this.lastPolyLine)
      this.map.remove(this.polygonAfterDraw)
    }
  }
}
</script>

<style scoped>
.map-page {
  height: 100%;
}
.map-container {
  height: 100%;
  width: 100%;
}
.btn-box {
  position: fixed;
  top: 50px;
  right: 80px;
  padding: 12px 15px;
  background: #fff;
  box-shadow: 0 0 8px 0 rgb(0 0 0 / 8%);
  border-radius: 4px;
  font-family: PingFangSC-Medium;
  font-size: 14px;
  color: #333;
  cursor: pointer;
  display: flex;
  justify-content: space-around;
}
.btn-box .btn {
  outline: none;
  border: none;
  font-size: 17px;
  color: #fff;
  border-radius: 4px;
  padding: 8px;
  background-color: #909399;
  cursor: pointer;
  transition: all 0.5s;
}
.btn-box .btn:hover {
  background-color: #409EFF;
}
.selectedArr{
  position: fixed;
  top: 150px;
  right: 80px;
  background: #fff;
  padding: 20px 20px 10px;
}
.selectedArr li{
  padding-bottom: 10px;
}
</style>