腾讯地图tmap使用总结(Vue2.x)
近期项目上需要用到地图做数据可视化,我经过试验选择了腾讯地图。这是腾讯地图的官网。
文件结构
- tmap.js 记录着地图的加载方法init
- tmap.vue 为地图组件
- infoWindow.js 消息弹窗的加载和销毁方法
- infoWindow.vue 消息弹窗的组件和他的自定义样式
加载
本项目采用了异步加载,在tmap.js声明了一个init方法,在tmap.vue调用这个方法。
// tmap.js
init: function () {
const AK = config.TMAP_KEY;
const TMap_URL =
'https://map.qq.com/api/gljs?v=1.exp&libraries=tools,service&key=' +
AK +
'&callback=onMapCallback';
return new Promise((resolve) => {
// 如果已加载直接返回
if (typeof window.TMap !== 'undefined') {
resolve(window.TMap);
return true;
}
// 地图异步加载回调处理
window.onMapCallback = function () {
resolve(window.TMap);
};
// 插入script脚本
let scriptNode = document.createElement('script');
scriptNode.setAttribute('type', 'text/javascript');
scriptNode.setAttribute('src', TMap_URL);
document.body.appendChild(scriptNode);
});
},
考虑到地图需要加载的文件很多,同时该项目中使用地图的地方也不少,我把地图放到了app.vue与router-view同级,防止其重复渲染,在不显示地图的情况下用v-show进行控制。
在调用加载方法时,担心他会多次加载脚本,采用了单例模式,因为 tmap中
init方法过后,腾讯地图的方法会放到window.TMap中,所以只需要判断window.TMap即可。
let TMap = null;
// 避免重复加载地图
if (window.TMap) TMap = window.TMap;
else {
const [res] = await awaitWrap(tmap.init());
console.log('loadMap');
TMap = res;
}
初始化
地图初始化则是调用 new TMap.Map方法,详情见下表(来自官网)
| 创建Map类的语法 | 参数 |
|---|---|
| new TMap.Map(domId, options); | domId : {string} (必填) 地图DOM容器的id,创建地图需要在页面中创建一个空div元素,传入该div元素的id options : {MapOptions} 地图参数,对象规范详见 MapOptions |
同样的,防止再次渲染地图,也采用了单例模式。初始化的时候:
mapStyleId是腾讯自定义地图中的样式Id,用户可以在腾讯个性地图自定义自己修改楼栋,陆地的样式。
baseMap是设置地图底图,BaseMap目前支持矢量底图(VectorBaseMap)、卫星底图(SatelliteBaseMap)、路况底图(TrafficBaseMap),可以使用数组形式实现多种底图叠加
另外,在CSDN看到一位大佬用document.querySelector('canvas+div:last-child')把商标和不必要的符号隐藏起来,就拿过来,参考和修改了一下。
if (this.tMap) {
this.tMap.setCenter(new TMap.LatLng(...position));
} else {
this.tMap = new TMap.Map('map-container', {
pitch: 60,
zoom: 18.5,
maxZoom: 19,
minZoom: 18,
viewMode: this.viewMode,
center: new TMap.LatLng(...position),
mapStyleId: 'style1',
baseMap: { features: ['building3d', 'base'] },
});
console.log('initMap', this.tMap);
this.hideTXIcon();
}
hideTXIcon() {
// 隐藏除信息窗体以外不必要的内容
var a = document.querySelector('canvas+div:last-child');
let div = a.children[a.childElementCount - 1];
div.style.display = 'none';
},
设置滑动范围和视野范围
产品大大说不能滑动到其他地方,所以设置了滑动范围和视野范围,感觉TX的这两个方法比gd好用,滑动范围是setBoundary,视野范围fitBounds,代码如下:
// 设置界限
let bounds = this.tMap.getBounds();
console.log(bounds);
const { _sw: sw, _ne: ne } = bounds;
let boundary = new window.TMap.LatLngBounds(sw, ne);
this.tMap.setBoundary(boundary); // 设置滑动范围
var latLngBounds = new window.TMap.LatLngBounds(sw, ne);
this.tMap.fitBounds(latLngBounds); //根据指定的范围调整地图视野
},
点标记
生成点标记marker的类为MultiMarker,我这里使用了他4个参数:
id,相同id的图层是不可以被重复添加map,点标记放在哪一个地图上styles点标记的样式,传入MarkerStyle类,只能支持特定的样式,比如换行whiteSpace就不支持了,详情看官网geometries点标记的POI信息,包括显示的坐标,高度,标记物的文案等(参考官网)
每个marker是都有一个id,这里的id我使用了与业务逻辑相关的值,为后续的信息窗体做准备。
addMarkers(arr = []) {
const getGeometory = (arr) => {
return arr.map((item) => {
const {
geometry: { position, title },
} = item;
console.log('getGeometory', this.infoMatchKey, item);
return {
//点标注数据数组
id: item[this.infoMatchKey],
spaceCode: item.space_code,
styleId: 'big',
position: new window.TMap.LatLng(...position, BUILDING_HEIGHT),
content: title,
};
});
};
// 添加标记
if (!this.tMap || !arr.length || !window.TMap) {
return;
}
if (this.markers) {
// 已存在 marker层,不需要创建,只需要添加点标记即可
const geos = getGeometory(arr);
this.markers.add(geos);
return;
}
let bigStyle = new window.TMap.MarkerStyle({
width: 36,
height: 54,
anchor: { x: 16, y: 32 },
src: this.markerImg,
color: '#fff',
size: 14,
offset: { x: 0, y: -8 },
whiteSpace: 'wrap',
});
let markers = new window.TMap.MultiMarker({
id: 'marker-layer',
map: this.tMap,
styles: {
//点标注的相关样式
// small: smallStyle,
big: bigStyle,
},
geometries: getGeometory(arr),
});
console.log('markers', markers);
this.markers = markers;
this.markers.on('click', (evt) => {
// let id = evt.geometry.id;
let { id: spaceId, spaceCode } = evt.geometry;
this.$router.push({
name: 'overview-building',
query: { spaceId, spaceCode },
});
});
return markers;
}
信息窗体
信息窗体时采用TMap.InfoWindow类生成,入参如下:
| map | Map | (必需)显示信息窗的地图。 |
|---|---|---|
| position | LatLng | (必需)信息窗的经纬度坐标。 |
| content | String | 信息窗显示内容,默认为空字符串。当enableCustom为true时,需传入信息窗体的dom字符串。 |
| zIndex | Number | 信息窗的z-index值,默认为0。 |
| offset | Object | 信息窗相对于position对应像素坐标的偏移量,x方向向右偏移为正值,y方向向下偏移为正值,默认为{x:0, y:0}。 |
| enableCustom | Boolean | 信息窗体样式是否为自定义,默认为false。 |
在做信息窗体的时候,我有遇到两个小问题:
- 窗体的样式过于复杂,如果使用官方的html模板写法,必定需要很多的工作量。
- 产品要求,当hover地图上的点标记时,要显示出标记对应的信息窗体
自定义消息窗体
幸好腾讯的地图的消息窗体是在地图canvas之外的DOM,在看了csdn某位大佬的帖子后。决定先把写一个消息窗体的vue组件,然后通过this.$refs.infoWindow.$el把该vue组件的元素注入到窗体中。
- 初始化窗体,由于窗体要完全自定义样式,所以设置
enableCustom为true,content设置一个id为CONTENT_BLOCK_ID的div(注意map_info_card一定要在全局样式上加宽度),代码如下:
// infoWindow.js
const createInfoWindow = (mapInstance, position, force = false) => {
if (!window.TMap || !mapInstance) {
return;
}
if (!modal || force) {
if (modal) modal.destroy();
modal = new window.TMap.InfoWindow({
map: mapInstance,
position: new window.TMap.LatLng(...position),
enableCustom: true,
offset: { x: 0, y: -50 }, //设置信息窗相对position偏移像素
content: `<div id="${CONTENT_BLOCK_ID}"></div>`,
});
}
modal.close();
return modal;
};
- 在infoWindow.js文件中添加一个设置窗体内容的方法:
// infoWindow.js
const setInfoWidnowContent = (dom) => {
const conatiner = document.getElementById(CONTENT_BLOCK_ID);
conatiner.append(dom);
};
- 创建
infoWindow.vue组件,这里省略一万字...,把组件引入地图组件中。
- 当云端返回接口信息窗体的内容后,通过组件内置方法设置内容,然后通过
setInfoWidnowContent把组件设置到TMap的信息窗体中。
// 通过云端接口初始化窗体信息
this.$refs.infoWindow.modalInit(rmap_popup);
// 设置窗体内容
setInfoWidnowContent(this.$refs.infoWindow.$el);
根据点标记显示对应窗体
云端决定把点标记和消息窗体分成两个接口返回,并都以Array的形式返回,我把云端数据放到vuex中,一个是markerList,另一个是infoWindowList,两个相关联的的字段我设置为matchKey,生成点标记的时候,我把marker的id设置为matchKey对应的值,marker的hover事件中,回调的入参evt会把点标记的geometry对象带上,通过匹配infoWindowList中的matchKey,找到对应的窗体内容,代码如下:
initInfoWindow() {
console.log('initInfoWindow');
this.markers &&
this.markers.on('hover', (evt) => {
console.log('hover evt', evt);
let isHover = !tmap.isMouseOutOfMarker(evt);
timer.switchTimer(TIEMR_EVENT, isHover);
isHover && this.setWindow(evt);
});
this.setWindowInfoDisInterval();
},
setWindow(node) {
if (!node) return false;
if (!node.geometry) {
node.geometry = {
title: node.space_name,
position: [node.lat, node.lng],
};
}
let res = this.setWindowContent(node);
res && this.openWindow(node.geometry.position);
},
openWindow(position) {
// 打开窗体
openInfoWindow(position);
this.$refs.infoWindow.showModal();
},
setWindowContent(node) {
// 设置窗体内容
const setContentFunc = () => {
let rmap_popup = this.infoWindowList.find(
(item) =>
item[this.infoMatchKey] == node.geometry.id ||
item[this.infoMatchKey] == node[this.infoMatchKey]
);
if (!rmap_popup) return false;
this.$refs.infoWindow.modalInit(rmap_popup);
setInfoWidnowContent(this.$refs.infoWindow.$el);
return true;
};
try {
return setContentFunc();
} catch (error) {
this.modal = createInfoWindow(
this.tMap,
[22.946829, 113.203575, BUILDING_HEIGHT],
true
);
return setContentFunc();
}
},
// 轮询
setWindowInfoDisInterval() {
if (!this.infoWindowList.length || !this.markerList.length) return;
let keys = this.infoWindowList.map((item) => item[this.infoMatchKey]);
const list =
this.markerList.filter((item) =>
keys.includes(item[this.infoMatchKey])
) || [];
if (!list.length) return;
let index = 0;
timer.addTimer(
TIEMR_EVENT,
() => {
this.setWindow(list[index]);
index++;
if (index === list.length) {
index = 0;
}
},
10000
);
}
事件监听
添加清除marker标记和关闭消息弹框的事件监听
clearMarkers() {
if (this.markers) {
this.markers.setMap(null);
this.markers = null;
}
},
clearInfoWindow() {
if (this.modal) {
this.modal.close();
}
},
initEvent() {
this.$eventBus.$on('map_clear', () => {
this.clearMarkers();
this.clearInfoWindow();
});
},
DOMMarkers
由于产品需要显示一些自定义的标记上去,并在鼠标hover上去显示特定的动效,我使用了腾讯地图的DOMOverlay进行开发。
腾讯地图的DOMOverlay是一个抽象类,里面有几个接口需要我去实现,接下来是实现接口的方法: 下面是接口文档:
| 抽象方法名 | 返回值 | 说明 |
|---|---|---|
| onInit(options) | None | 实现这个接口来定义构造阶段的初始化过程,此方法在构造函数中被调用,接收构造函数的options参数作为输入 |
| onDestroy() | None | 实现这个接口来定义销毁阶段的资源释放过程,此方法在destroy函数中被调用 |
| createDOM() | HTMLElement | 实现这个接口来创建自定义的DOM元素,此方法在构造函数中被调用(在初始化过程之后) |
| updateDOM() | None | 实现这个接口来更新DOM元素的内容及样式,此方法在地图发生平移、缩放等变化时被调用 |
onInit
// 绑定用户配置入参
onInit({ map, groups, evts, mapId }) {
Object.assign(this, {
map,
groups,
evts,
mapId,
});
}
createDOM
const setMarkerHtml = ({ name, disType, pointType, text }) => {
// 按照业务逻辑要求显示对应的节点html
let html = '';
switch (disType) {
case 'pic-text':
html = `<div class="pic-text map-dom-marker ${name} ${pointType}"><div class=${
!isIos() && 'font-10'
}>${text}<div></div>`;
break;
case 'pic-only':
html = `<div class="pic-only map-dom-marker ${name} ${pointType}">
<img class="type-img" src="${require(`@/assets/images/${name}.png`)}"/>
<div class="bottom-img "></div>
</div>`;
break;
case 'pic-one':
html = `<div class="pic-one map-dom-marker ${name} ${pointType}">
<div class="point"></div>
</div>`;
break;
}
return html;
};
// 建立DOM元素,返回一个Element,使用this.dom能够获取到这个元素
createDOM() {
// 创建一个事件绑定的工厂
const eventListenerFactory = (evtName, ele) => {
this['on' + evtName] = function (e) {
this.emit(evtName, e.target.firstChild || e.target.parentElement);
}.bind(this);
ele.addEventListener(evtName, this['on' + evtName]);
};
let dom = null;
if (this.groups instanceof Array) {
if (this.groups.length) {
dom = document.createElement('div');
// 业务逻辑 样式定制
if (
this.groups[0].pointType &&
this.groups[0].pointType.indexOf('charge') > -1
) {
dom.className = 'dom-block-charge';
}
this.groups.forEach((item) => {
let elem = document.createElement('div');
elem.className = `dom-container ${item.pointType}`;
// 写innerHtml
elem.innerHTML = setMarkerHtml(item);
elem && dom.appendChild(elem);
this.evts.forEach((evtName) => {
eventListenerFactory(evtName, elem);
});
});
}
}
return dom;
}
updateDOM 和 destroy
// 需要添加防抖
updateDOM() {
if (!this.map) {
return;
}
if (!(this.groups instanceof Array)) return;
this.groups.forEach((item, index) => {
// 地图移动或者缩放的时候,DOMOverlay的position会相应改动,我们通过地图提供的方法projectToContainer获取对应的像素
// 经纬度坐标转容器像素坐标
// console.log('updateDOM', this);
const { position, disType } = item;
let pixel = this.map.projectToContainer(position);
// 业务逻辑 修改样式
const domWidth = disType === 'pic-text' ? '60px' : '80px';
this.dom.children[index].style.width = domWidth;
// 使饼图中心点对齐经纬度坐标点
// 用translate 而不直接修改top,left,性能优化
let left = pixel.getX() - this.dom.children[index].clientWidth / 2 + 'px';
let top = pixel.getY() - this.dom.children[index].clientHeight + 'px';
this.dom.children[index].style.transform = `translate(${left}, ${top})`;
});
}
// 销毁时
onDestroy() {
if (this.onClick) {
this.dom.removeEventListener(this.onClick);
}
}
构建和使用
当腾讯地图包加载进来之产生的回调引入并导出DomMarkerClass
文件tmap.js截图
// tmap.vue代码
// 标记创建以及事件绑定
let markers = new DomMarkerClass.default({
map: this.tMap,
groups: doms, // 传入描点位置列表
evts: evts, //['mouseenter', 'mouseleave', 'click']
mapId: this.id,
});
if (arr[0] == DOM_MARK_POS[0]) {
// 统一事件交互处理
markers.on('mouseenter', (e) => {
this.setHeightLightDom(e.classList[2]);
});
markers.on('mouseleave', () => {
this.currentHLDomName = '';
this.setHeightLightDom('');
// e.classList.remove('selected');
});
markers.on('click', (e) => {
let domName = e.classList[2];
const routerMap = new Map([
['new_energy', '/energy-charging'],
['street_lamp', '/photovoltaic-power'],
['rain_pool', '/rain-collect'],
['elevator_area', '/elevator-feedback'],
]);
this.$router.push(routerMap.get(domName));
});