在Vue上进行开发,地图使用了百度提供的Vue Baidu Map。当前版本为v0.21.15。官方文档地址:
https://dafrok.github.io/vue-baidu-map/#/zh/index
开发需求:在百度地图上动态进行多边形的显示,添加,删除,修改。
具体流程:
- 在地图上点右键,弹出菜单:添加多边形。点击菜单项后,进入绘制模式。
- 在绘制模式下,每次鼠标点击百度地图,都会在图上添加一个点。多次添加的点构成一个多边形。
- 随着鼠标在地图上移动,多边形的最后一个点也随之改变。
- 点击右键,弹出菜单:添加结束。点击菜单项后,多边形绘制完成。
- 在多边形上点右键,弹出菜单:修改多边形,删除多边形。
- 选择修改多边形,则多边形进入修改模式,可用鼠标拖动其每一个点。
- 选择删除多边形,则会将多边形从地图中删除。
- 可添加多个多边形,每个多边形的菜单互不影响。
分析整个流程,可以得出要解决的关键点有以下几个:
- 为地图添加右键菜单。
- 为地图添加左键点击事件和mousemove事件。
- 在地图上显示多边形。
- 在多边形上添加右键菜单。
- 将功能封装为一个单独的vue。
关键点
Vue Baidu Map实际是百度官方对百度JavaScript API v2.0进行的封装,动态为document创建了一个<script>
标签并下载JavaScript API v2.0,因此其核心功能依然是JavaScript API v2.0提供的,Vue Baidu Map只是一层壳而已。
Vue Baidu Map包括BaiduMap在内的组件都是异步加载,加载完成后会触发ready事件。ready事件传入的参数为{BMap, map}
,因此在ready中可以拿到BMap与实例化后的map对象。既然有了这两个对象,那么对地图的操作完全可以使用常规html页面中调用百度JavaScript API v2.0的方式。
为地图添加右键菜单
官方文档中明确给出了为地图添加右键菜单的方式,即使用BmContextMenu组件:
<baidu-map class="map" :center="center" :zoom="zoom">
<bm-context-menu>
<bm-context-menu-item :callback="addPolygon" text="添加多边形"></bm-context-menu-item>
</bm-context-menu>
</baidu-map>
使用组件的方式非常简单明了。
除了组件的方式,也可以在BaiduMap的@ready
事件中使用js的方式添加:
mapReady({BMap, map}) {
var addPolygon = function() {}
var menu = new BMap.ContextMenu()
menu.addItem(new BMap.MenuItem('添加多边形', addPolygon))
map.addContextMenu(menu)
}
添加后,在地图上点右键,菜单就会弹出。
为地图添加左键点击事件和mousemove事件
左键点击事件可以直接为BaiduMap绑定@click
事件。
若使用js的方式,可以用addEventListener()
来添加:
var clickCallback = function(e) {}
map.addEventListener('click', clickCallback)
mousemove事件同理:
var mousemoveCallback = function(e) {}
map.addEventListener('mousemove', mousemoveCallback)
在地图上显示多边形
在地图上显示多边形可以使用BmPolygon组件:
<baidu-map class="map" :center="center" :zoom="zoom">
<bm-polygon
v-for="(item, index) in polygons"
:path="item.path"
:editing="item.editing"
:key="index">
</bm-polygon>
</baidu-map>
其中polygons是在data中定义的一个数组,其每个数据的结构为:
{
path: [], // 所有点
editing: false, // 是否可编辑
}
这样就可以在地图上显示多个多边形了。
在多边形上添加右键菜单
在百度地图的定义中,多边形属于覆盖物(overlay)。为覆盖物添加右键菜单,js的方式为:
var editPolygon = function() {}
var deletePolygon = function() {}
var menu = new BMap.ContextMenu()
menu.addItem(new BMap.MenuItem('修改', editPolygon.bind(polygonObj)))
menu.addItem(new BMap.MenuItem('删除', deletePolygon.bind(polygonObj)))
polygonObj.addContextMenu(menu)
其中polygonObj
为多边形对象。
尝试使用BmContextMenu来为多边形添加菜单:
<bm-polygon
:path="path"
:editing="editing"
<bm-context-menu>
<bm-context-menu-item :callback="editPolygon" text="修改"></bm-context-menu-item>
<bm-context-menu-item :callback="deletePolygon" text="删除"></bm-context-menu-item>
</bm-context-menu>
</bm-polygon>
实际却会报错。
这是因为BmPolygon组件本身并没有提供对BmContextMenu的支持。官方给出的覆盖物添加菜单的例子是BmMarker的:
https://dafrok.github.io/vue-baidu-map/#/zh/context-menu/menu
于是查看BmMarker的源码:
<template>
<div>
<slot></slot>
</div>
</template>
<script>
...
</script>
再查看BmPolygon的源码:
<script>
...
</script>
可见,BmMarker预留了一个<slot>
给可能塞进来的元素,而BmPolygon则没有。因此将BmMarker的<template>
部分复制给BmPolygon,再次编译运行,发现右键点击BmPolygon,会弹出菜单了,且其回调函数可以正常响应。
更新问题
多边形需要动态绘制。此时出现了一个问题:若页面初始化时多边形的各个点就已经确定,那么多边形点右键会弹出菜单。然而,若多边形是动态添加的,则点右键不会弹出菜单。
当多边形动态添加或修改时,会构造一个全新的对象。该对象与之前的多边形是独立的。而菜单依然依附在之前的多边形对象上。于是当菜单进行reload时就会有问题。
于是考虑放弃使用BmContextMenu,改为当在多边形上点右键时,调用js API动态生成多边形的菜单。
所以现在的问题变为:为多边形添加右键点击事件。
为多边形添加右键点击事件
查询js的事件可知,鼠标右键点击事件名为rightclick
。然而直接为BmPolygon添加@rightclick
响应函数,并不会触发。于是可断定BmPolygon并没有触发该事件。
实际上,官方给出的js API的demo里,覆盖物本身并不监听这些事件,需要调用addEventListener()
来为其注册事件。于是封装的BmPolygon没有触发该事件也就理所当然了。
查看BmPolygon的源码,大致结构为:
<script>
...
export default {
name: 'BmPolygon',
mixins: [commonMixin('overlay')],
props: {...},
watch: {...},
methods: {
load() {
const { BMap, map, path, ... } = this
const overlay = new BMap.Polygon(
...
})
this.originInstance = overlay
map.addOverlay(overlay)
bindEvents.call(this, overlay)
...
}
}
}
</script>
可以看到在load函数中调用js API来new了一个Polygon并添加给map对象,并将其保存在this.originInstance中。这个Polygon对象就是整个组件的核心对象。
于是,要为Polygon对象注册rightclick
事件,就在这里对overlay调用addEventListener()
即可:
load() {
...
const that = this
overlay.addEventListener('rightclick', function(e) {
that.$emit('rightclick', e)
})
}
注意需要同步为BmPolygon触发一个rightclick
,这样通过@rightclick
注册给BmPolygon的函数才能响应。
其中参数e与click的参数e格式相同,其e.currentTarget为当前覆盖物js对象。
右键事件注册成功,剩下的就是在右键事件中调用js API为当前多边形对象添加菜单了。
添加给多边形的菜单,只有当多边形更新时才会失效。若多边形的点一直没有变化,那么添加给它的菜单也会一直都能响应。考虑到这一点,将菜单对象保存在多边形对象中,并添加一个if判断:
onRightClick(e) {
var polygonObj = e.currentTarget
if(polygonObj.menu === undefined) {
var menu = new BMap.ContextMenu() // BMap需自行保存
menu.addItem(new BMap.MenuItem('修改', function() {
...
}))
polygonObj.addContextMenu(menu)
// 将menu保存在多边形对象中
polygonObj.menu = menu
// 添加菜单后不会立即弹出,需额外触发一次右键点击事件
polygonObj.dispatchEvent('rightclick', e)
}
}
这样,只有当多边形更新时才会创建新的菜单。
需要注意的一点是,用户在多边形上点击右键,是希望看到弹出菜单。然而,多边形右键的触发顺序是:若有菜单,先弹出菜单→触发rightclick函数。因此,若多边形此时没有菜单,尽管会在rightclick中创建并添加,但由于菜单的显示流程在前,所以菜单是不会弹出来的。于是在最后额外触发了一次右键点击事件,这次事件触发后,由于菜单已有,所以会正常弹出;再次触发rightclick函数,因为polygonObj.menu也不为undefined了,所以什么也不做。
对于用户的感知就是,点击矩形区域,弹出了菜单。
封装
功能已经实现了,接下来要对整体功能进行封装。然而遇到了一个新的问题:
当把BmPolygon直接放在BaiduMap组件中时,显示一切正常:
<baidu-map class="map" :center="center" :zoom="zoom">
<bm-polygon
v-for="(item, index) in polygons"
:path="item.path"
:editing="item.editing"
:key="index">
</bm-polygon>
</baidu-map>
若将BmPolygon封装到一个PolygonEx.vue文件中,用<div>
进行包裹,然后再引入到BaiduMap组件中时,多边形不显示了:
<template>
<div>
<bm-polygon
v-for="(item, index) in polygons"
:path="item.path"
:editing="item.editing"
:key="index">
</bm-polygon>
</div>
</template>
考虑到封装的PolygonEx.vue类似一个overlay组件,于是分析了一下Vue Baidu Map的overlay组件写法:
<script>
import commonMixin from '../base/mixins/common.js'
export default {
...
mixins: [commonMixin('overlay')],
...
methods: {
load () {
const {BMap, map, $el, pane} = this
...
const overlay = new XXX()
this.originInstance = overlay
map.addOverlay(overlay)
}
}
}
</script>
如上,其结构都有如下特点:
- 都混入了一个commonMixin。
- 都提供了一个
load()
函数。
查看commonMixin源码,发现其ready函数为:
ready () {
const $parent = getParent(this.$parent)
const BMap = this.BMap = $parent.BMap
const map = this.map = $parent.map
this.load()
this.$emit('ready', {
BMap,
map
})
}
其中调用了this.load()
。所以load()函数是必须实现的。
因此,要自己实现一个覆盖物组件PolygonEx.vue,只需要混入commonMixin,并提供load()
函数即可。
按这个规则修改PolygonEx.vue,多边形可以正常显示了。
其他
上面已经解决了最主要的一些问题。还有一些其他的细节,例如令矩形随鼠标移动而动态修改最后一个点,矩形不同状态下弹出的菜单不同,多个矩形之间状态独立菜单互不影响,矩形数组改变则所有矩形重绘等,都是需要考虑的。具体可以参考下面的源码。
源码
在页面的index.vue所在目录下,添加一个components文件夹,在其下添加两个vue文件:Polygon.vue和PolygonEx.vue。
Polygon.Vue
<template>
<div>
<slot/>
</div>
</template>
<script>
import commonMixin from 'vue-baidu-map/components/base/mixins/common.js'
import bindEvents from 'vue-baidu-map/components/base/bindEvent.js'
import { createPoint } from 'vue-baidu-map/components/base/factory.js'
export default {
name: 'BmPolygon',
mixins: [commonMixin('overlay')],
props: {
path: {
type: Array,
default() {
return []
}
},
strokeColor: {
type: String,
default: '#CC3333'
},
strokeWeight: {
type: Number,
default: 2
},
strokeOpacity: {
type: Number,
default: 0.7
},
strokeStyle: {
type: String,
default: 'solid'
},
fillColor: {
type: String,
default: '#00CCFF'
},
fillOpacity: {
type: Number,
default: 0.1
},
massClear: {
type: Boolean,
default: true
},
clicking: {
type: Boolean,
default: true
},
editing: {
type: Boolean,
default: false
}
},
watch: {
path: {
handler(val, oldVal) {
this.reload()
},
deep: true
},
strokeColor(val) {
this.originInstance.setStrokeColor(val)
},
strokeOpacity(val) {
this.originInstance.setStrokeOpacity(val)
},
strokeWeight(val) {
this.originInstance.setStrokeWeight(val)
},
strokeStyle(val) {
this.originInstance.setStrokeStyle(val)
},
fillColor(val) {
this.originInstance.setFillColor(val)
},
fillOpacity(val) {
this.originInstance.setFillOpacity(val)
},
editing(val) {
val ? this.originInstance.enableEditing() : this.originInstance.disableEditing()
},
massClear(val) {
val ? this.originInstance.enableMassClear() : this.originInstance.disableMassClear()
},
clicking(val) {
this.reload()
}
},
methods: {
load() {
const { BMap, map, path, strokeColor, strokeWeight, strokeOpacity, strokeStyle, fillColor, fillOpacity, editing, massClear, clicking } = this
const overlay = new BMap.Polygon(path.map(item => createPoint(BMap, { lng: item.lng, lat: item.lat })), {
strokeColor,
strokeWeight,
strokeOpacity,
strokeStyle,
fillColor,
fillOpacity,
// enableEditing: editing,
enableMassClear: massClear,
enableClicking: clicking
})
this.originInstance = overlay
map.addOverlay(overlay)
bindEvents.call(this, overlay)
const that = this
overlay.addEventListener('rightclick', function(e) {
that.$emit('rightclick', e)
})
// 这里有一个诡异的bug,直接给 editing 赋值时会出现未知错误,因为使用下面的方法抹平。
editing ? overlay.enableEditing() : overlay.disableEditing()
}
}
}
</script>
PolygonEx.Vue
<template>
<div>
<bm-polygon
v-for="(item, index) in polygons"
:path="item.path"
:editing="item.editing"
:key="index"
@ready="onReady($event)"
@rightclick="rightClick(index, $event)"/>
</div>
</template>
<script>
import commonMixin from 'vue-baidu-map/components/base/mixins/common.js'
import BmPolygon from './Polygon'
export default {
name: 'BmPolygonEx',
components: { BmPolygon },
mixins: [commonMixin('overlay')],
props: {
polygonData: {
type: Array,
default() {
return []
}
}
},
data() {
return {
polygons: []
}
},
watch: {
polygonData(newPolygonData, oldPolygonData) {
this.reload()
}
},
methods: {
load() {
// 由于mix调用,该函数必须存在
const { BMap, map } = this
// 防止重复添加菜单
if (map.menu === undefined) {
var that = this
var { polygons } = this
// 添加多边形
var addPolygonCallback = function() {
polygons.push({
editing: false,
state: 'pre_draw',
path: []
})
}
var menu = new BMap.ContextMenu()
menu.addItem(new BMap.MenuItem('添加多边形', addPolygonCallback))
map.addContextMenu(menu)
// 暂存菜单
map.menu = menu
// 点击地图
var clickCallback = function(e) {
var polygon = polygons[polygons.length - 1]
switch (polygon.state) {
case 'pre_draw':
polygon.path.push(e.point)
polygon.state = 'draw'
break
case 'draw':
polygon.path.push(e.point)
break
default:
break
}
}
// 鼠标移动
var mousemoveCallback = function(e) {
if (polygons.length > 0 && polygons[polygons.length - 1].state === 'draw') {
var path = polygons[polygons.length - 1].path
if (path.length === 1) {
path.push(e.point)
} else if (path.length > 1) {
// 调用vue的$set修改数组,可触发更新
that.$set(path, path.length - 1, e.point)
}
}
}
map.addEventListener('click', clickCallback)
map.addEventListener('mousemove', mousemoveCallback)
}
var polygons = []
if (this.polygonData.length > 0) {
this.polygonData.forEach(function(item, index, array) {
polygons.push({
state: 'idle',
editing: false,
path: item.path,
data: item
})
})
}
this.polygons = polygons
},
onReady({ BMap, map }) {
},
rightClick(index, e) {
var polygonObj = e.currentTarget
if (polygonObj.menu === undefined) {
console.log('%c 菜单不存在,创建', 'color: #CC00FF;')
var that = this
var { polygons, BMap } = this
var menus = []
// 变更状态函数
var changeState = function(newState) {
polygons[index].state = newState
if (polygonObj.menu !== undefined) {
polygonObj.removeContextMenu(polygonObj.menu)
}
// 一旦状态变更,弹出的菜单也会变更。故将菜单置空,等显示菜单时重建
polygonObj.menu = undefined
}
switch (polygons[index].state) {
case 'idle':
menus.push({
title: '修改多边形',
callback: function() {
console.log('%c 修改多边形', 'color: #CC00FF;')
polygons[index].editing = true
changeState('modify')
}
})
menus.push({
title: '删除多边形',
callback: function() {
console.log('%c 删除多边形', 'color: #CC00FF;')
if (polygons.length > index) {
// 删除多边形需要先触发事件,否则数据删掉就无法返回了
that.$emit('delete', { ...polygons[index] })
polygons.splice(index, 1)
}
}
})
break
case 'draw':
menus.push({
title: '添加结束',
callback: function() {
console.log('%c 添加结束', 'color: #CC00FF;')
changeState('idle')
// 移除最后一个点,该点由鼠标移动生成
polygons[polygons.length - 1].path.pop()
if (polygons[polygons.length - 1].path.length < 3) {
polygons.pop()
} else {
that.$emit('drawover', polygons[polygons.length - 1])
}
}
})
break
case 'modify':
menus.push({
title: '修改结束',
callback: function() {
console.log('%c 修改结束', 'color: #CC00FF;')
changeState('idle')
polygons[index].editing = false
that.$set(polygons[index], 'path', polygonObj.getPath())
that.$emit('modifyover', polygons[index])
}
})
break
default:
break
}
var menu = new BMap.ContextMenu()
menus.forEach(function(item, index, array) {
menu.addItem(new BMap.MenuItem(item.title, item.callback.bind(polygonObj)))
})
polygonObj.addContextMenu(menu)
polygonObj.menu = menu
// 添加菜单后不会立即弹出,需额外触发一次右键点击事件
polygonObj.dispatchEvent('rightclick', e)
}
}
}
}
</script>
index.vue
<template>
<div class="page-main">
<div ref="test" class="test">
<button @click="doClick">重置矩形区</button>
</div>
<baidu-map ref="bdMap" v-bind="mapOptions" class="map-container">
<bm-polygon-ex
:polygon-data="polygonData"
@drawover="callbakAdd"
@modifyover="callbakModify"
@delete="callbakDelete"/>
</baidu-map>
</div>
</template>
<script>
import {
BaiduMap,
BmContextMenu,
BmContextMenuItem
} from 'vue-baidu-map/components'
import BmPolygonEx from './components/PolygonEx'
export default {
components: { BaiduMap, BmPolygonEx, BmContextMenu, BmContextMenuItem },
data() {
return {
mapOptions: {
ak: 'xxxxxxxxxx',// 替换为自己的ak
center: {
lng: 116.405994,
lat: 39.914935
},
scrollWheelZoom: true,
zoom: 16
},
polygonData: [{
path: [{ 'lng': 116.389932, 'lat': 39.924204 }, { 'lng': 116.395538, 'lat': 39.920856 }, { 'lng': 116.395142, 'lat': 39.915544 }, { 'lng': 116.389609, 'lat': 39.918089 }],
value: 0
}]
}
},
methods: {
doClick() {
console.log('%c doClick', 'color: blue;')
this.polygonData = [{
path: [{ 'lng': 116.389932, 'lat': 39.924204 }, { 'lng': 116.395538, 'lat': 39.920856 }, { 'lng': 116.395142, 'lat': 39.915544 }, { 'lng': 116.389609, 'lat': 39.918089 }],
value: 0
}]
},
callbakAdd(e) {
console.log('%c map callbakAdd', 'color: green;')
console.log(e)
},
callbakModify(e) {
console.log('%c map callbakModify', 'color: green;')
console.log(e)
},
callbakDelete(e) {
console.log('%c map callbakDelete', 'color: green;')
console.log(e)
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.page-main {
width: 100%;
height: 100%;
}
.map-container {
width: 100%;
height: 800px;
}
</style>