腾讯地图tmap使用总结(Vue2.x)

4,954 阅读5分钟

腾讯地图tmap使用总结(Vue2.x)

近期项目上需要用到地图做数据可视化,我经过试验选择了腾讯地图。这是腾讯地图的官网

文件结构

image-20220602135631905.png

  • 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进行控制。

image.png 在调用加载方法时,担心他会多次加载脚本,采用了单例模式,因为 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类生成,入参如下:

mapMap(必需)显示信息窗的地图。
positionLatLng(必需)信息窗的经纬度坐标。
contentString信息窗显示内容,默认为空字符串。当enableCustom为true时,需传入信息窗体的dom字符串。
zIndexNumber信息窗的z-index值,默认为0。
offsetObject信息窗相对于position对应像素坐标的偏移量,x方向向右偏移为正值,y方向向下偏移为正值,默认为{x:0, y:0}。
enableCustomBoolean信息窗体样式是否为自定义,默认为false。

在做信息窗体的时候,我有遇到两个小问题:

  • 窗体的样式过于复杂,如果使用官方的html模板写法,必定需要很多的工作量。
  • 产品要求,当hover地图上的点标记时,要显示出标记对应的信息窗体

自定义消息窗体

幸好腾讯的地图的消息窗体是在地图canvas之外的DOM,在看了csdn某位大佬的帖子后。决定先把写一个消息窗体的vue组件,然后通过this.$refs.infoWindow.$el把该vue组件的元素注入到窗体中。

  1. 初始化窗体,由于窗体要完全自定义样式,所以设置enableCustomtruecontent设置一个idCONTENT_BLOCK_IDdiv(注意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;
};
  1. 在infoWindow.js文件中添加一个设置窗体内容的方法:
// infoWindow.js
const setInfoWidnowContent = (dom) => {
  const conatiner = document.getElementById(CONTENT_BLOCK_ID);
  conatiner.append(dom);
};
  1. 创建infoWindow.vue组件,这里省略一万字...,把组件引入地图组件中。

image-20220606114109572.png

  1. 当云端返回接口信息窗体的内容后,通过组件内置方法设置内容,然后通过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;
  }

updateDOMdestroy

  // 需要添加防抖
  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截图 image.png

// 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));
        });