vue中使用Leaflet加天地图服务

1,002 阅读8分钟

前言

项目需要使用Leaflet加天地图的服务,具体实现不同类型图层的切换,地图打点点位信息编辑,点击点位显示提示框、点的缩放聚合。

使用vue2、vue-cli、leaflet;这个vue react都行这个地图库也就是js操作随便用哪个都可以地图实例能获取到就可以操作。

源码地址:gitee.com/yin-chunyan…

  1. leaflet 中文网址 leafletjs.cn/reference.h…
  2. leaflet.markercluster www.npmjs.com/package/lea…
  3. element-ui element.eleme.io/#/zh-CN/com…
  • 默认数据 和聚合2个

    image.png

  • 点击点位

    image.png

  • 点击编辑

    image.png

  • 点击新增按钮 后点击地图

    image.png

一、准备

1.下载 leaflet、leaflet.markercluster

npm install leaflet --save
npm install leaflet.markercluster --save

在使用的地方引入

import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet.markercluster';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';

2.下载 element-ui

npm i element-ui -S

main.js注册

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';

Vue.use(ElementUI);

new Vue({
  el: '#app',
  render: h => h(App)
});

3.申请天地图key

sso.tianditu.gov.cn/login?servi…

注册登录创建新的应用得到key;

image.png

二、Leaflet库的初始和方法

1.准备创建一个methods.js引入Leaflet相关库;存放天地图服务地址和key

引入leaflet库,和一张图片用来作为marker图标

import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet.markercluster';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';

//! 引入一张图片 作为marker图标
import icon from '../images/xiaolong.gif';

存放天地图服务key,和地图服务地址数组;服务地址数组方便类型切换设置地图图层;

spherical:球面墨卡托投影, LatLong:球面墨卡托投影, mapLabel:地图注记服务

//! 天地图服务Key
export const KEY = `512e6b397b64c5f690cf9cca2925da1f`;

//! 天地图图层信息数组
export const layerList = [
    {
        name: "矢量底图",
        spherical: `http://t{s}.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=${KEY}`,
        LatLong: `http://t{s}.tianditu.gov.cn/DataServer?T=vec_c&x={x}&y={y}&l={z}&tk=${KEY}`,
        mapLabel: {
            name: "矢量注记",
            spherical: `http://t{s}.tianditu.gov.cn/DataServer?T=cva_w&x={x}&y={y}&l={z}&tk=${KEY}`,
            LatLong: `http://t{s}.tianditu.gov.cn/DataServer?T=cva_c&x={x}&y={y}&l={z}&tk=${KEY}`,
        }
    },
    {
        name: "影像底图",
        spherical: `http://t{s}.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=${KEY}`,
        LatLong: `http://t{s}.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=${KEY}`,
        mapLabel: {
            name: "影像注记",
            spherical: `http://t{s}.tianditu.gov.cn/DataServer?T=cia_w&x={x}&y={y}&l={z}&tk=${KEY}`,
            LatLong: `http://t{s}.tianditu.gov.cn/DataServer?T=cia_c&x={x}&y={y}&l={z}&tk=${KEY}`,
        }
    },
    {
        name: "地形晕渲",
        spherical: `http://t{s}.tianditu.gov.cn/DataServer?T=ter_w&x={x}&y={y}&l={z}&tk=${KEY}`,
        LatLong: `http://t{s}.tianditu.gov.cn/DataServer?T=ter_c&x={x}&y={y}&l={z}&tk=${KEY}`,
        mapLabel: {
            name: "影像注记",
            spherical: `http://t{s}.tianditu.gov.cn/DataServer?T=cta_w&x={x}&y={y}&l={z}&tk=${KEY}`,
            LatLong: `http://t{s}.tianditu.gov.cn/DataServer?T=cta_c&x={x}&y={y}&l={z}&tk=${KEY}`,
        }
    },
    {
        name: "全球境界",
        spherical: `http://t{s}.tianditu.gov.cn/DataServer?T=ibo_w&x={x}&y={y}&l={z}&tk=${KEY}`,
        LatLong: `http://t{s}.tianditu.gov.cn/DataServer?T=ibo_c&x={x}&y={y}&l={z}&tk=${KEY}`,
        mapLabel: null
    },
]

2.初始化地图方法

实现initMap方法,传入el元素返回L.Map实例对象

选项小多请查看 L.map方法

/**
 * 初始化 Leaflet 地图
 *
 * @param {HTMLElement} el - 地图容器的元素或元素的ID
 * @returns {L.Map} 返回一个初始化后的 Leaflet 地图实例
 */
export const initMap = (el) => {
    return L.map(el,
        {
            center: [39.912565, 116.408509],
            zoom: 5,
            maxZoom: 18,
            zoomControl: true,
            zoomAnimation: true,

        }
    );
}

3.设置图层方法

由于需要切换图层类型,显示不同的图层需要实现两个方法:

  1. 删除地图中所有图层信息 clearLayers 方法

    传入地图实例,通过实例方法eachLayer 遍历所有图层对象

    通过判断图层对象是否含有 formState key区分是否是marker

    后续使用到聚合功能使用的插件,这里单独一个图层需要清除

  2. 设置图层方法 setLayer

    传入地图实例、天地图服务对象,通过L.tileLayer方法实例图层对象

    由该实例方法addTo(map)添加到地图中

    创建聚合图层实例,将该图层实例添加地图实例一个值上方便后面操作添加点位

/**
 * 删除地图的所有 图层,用于图层类型的切换
 *
 * @param {L.Map} map - 地图容器的元素或元素的ID
 */
export const clearLayers = (map) => {
    map.eachLayer(l => {
        if (!('formState' in l)) {        //! 如果有 formState 表示图标图标这里不删除
            map.removeLayer(l);
        }
    })

    // 清除聚合层中的所有marker
    if (map.markers) {
        map.markers.clearLayers();
    }
}


/**
 * 设置图层方法
 *
 * @param {L.Map} map - 地图容器的元素或元素的ID
 * @param {Object} layerObject - layerList[n]对象
 * */

//! 1. 每次先调用 clearLayers 清楚所有图层
//! 2. 完了之后在使用 titleLayer 设置图层对象, addTo添加到地图当中

export const setLayer = (map, layerObject) => {
    clearLayers(map);
    const {spherical, mapLabel} = layerObject;
    let layer = L.tileLayer(spherical, {
        subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
        maxZoom: 18,
        minZoom: 1,
    });
    layer.addTo(map);

    //! 有些地图图层类型,可能没有 label 标注 这里判断
    if (mapLabel && Object.keys(mapLabel).length !== 0) {
        const {spherical: sphericalLabel} = mapLabel;
        let layerLabel = L.tileLayer(sphericalLabel, {
            subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
            maxZoom: 18,
            minZoom: 1
        });
        layerLabel.addTo(map);
    }

    // 创建marker聚合层
    const markers = L.markerClusterGroup();
    // 创建一个自定义的LayerGroup来控制层级
    const layerGroup = L.layerGroup([markers]);
    layerGroup.addTo(map);
    // 设置层级,注意这个方法会将图层在地图中移到指定位置
    layerGroup.setZIndex(9999999999999999999999); // 设置图层的层级
    // 将markers保存到map对象上,以便后续使用
    map.markers = markers;
    map.layerGroup = layerGroup;
}

4.设置地图事件方法

传入地图实例,和事件名对象以回调形式使用

//! 地图事件的添加
export const MapEvents = (map, events) => {
    map.on('click', events.click)
    map.on('zoom', events.zoom)
}

5.添加点位方法

存放点位配置对象

L.marker方法配置信息

//! marker 配置信息
const markerOption = {
    draggable: true,         //是否允许拖拽点
    icon: L.icon({
        iconUrl: icon,
        iconSize: [27, 27],
        iconAnchor: [14, 14],
    })
}

点位添加方法

传入表单对象,用于存放点位信息和提示窗回显内容

地图实例用于添加点位到地图中,这里就用到设置图层方法中设置的聚合图层值

传入的list 用来存放所有marker的数组 用来保存点位

传入事件名对象,操作点位的事件

/**
 * 添加点位方法
 *
 * @param {Object} form - 点位表单信息对象
 * @param {L.Map} map - 地图实例对象
 * @param {Array} list - 存放所有点位对象数组,用来保存提交接口时
 * @param {Object} event - 事件对象包含 click dblclick 等等;采用回调形式
 * */
const addMarkerItem = (form, map, list, event) => {
    let latlng = Object.values(form.latlng);
    let marker = L.marker(latlng, markerOption);
    marker['formState'] = form;
    //! 点击打开 信息提示
    marker.on('click', event.click);
    //! 双击编辑
    marker.on('dblclick', event.dblclick);
    marker.on('dragend', event.dragend);
    marker.on('move', event.move);
    list.push(marker);
    // 将marker添加到聚合层
    marker.addTo(map.markers);  //! 这里要有聚合功能就必须添加到这里
}

6.编辑点位方法对象

用来调用新增list,新增单个和编辑点位表单信息的对象

export const editMarkerMethod = {
    addList(formStates, map, list, event) {
        formStates.map((form) => {
            addMarkerItem(form, map, list, event)
        })
    },
    add(form, map, list, event) {
        addMarkerItem(form, map, list, event)
    },
    edit(form, list) {
        let item = list.find((v) => {
            if (v.formState.timeId === form.timeId) return v;
        })
        item.formState = {...form};
    }
}

7.添加信息窗(显示信息窗)

信息窗只能展示一个,这里用一个变量POPUP接收实例,后面用于删除信息窗(切换图层类型时)

addPopup 显示信息窗方法,传入地图实例,和表单对象,事件名对象

  • 表单对象用来回显内容
  • 时间名对象,用来编辑表单信息和删除操作

removePopup 切换不同的图层类型,删除掉信息窗

//! popup 信息窗 只会展示一个,这里保存最新的;用来关闭使用
let POPUP = null;

//! 添加 信息窗;设置了编辑。删除功能 针对于点位的操作
export const addPopup = (map, formState, events) => {
    let {latlng} = formState;

    let dom = document.createElement("div");
    dom.innerHTML = `
        <a class="setLink">编辑</a>
        <a class="delLink">删除</a>
        <div>名字:${formState.name}</div>
        <div>活动区域:${formState.region}</div>
        <div>活动形式:${formState.hotType}</div>
    `
    dom.querySelector('.setLink').onclick = () => {
        events.click && events.click(formState);
    }

    dom.querySelector('.delLink').onclick = () => {
        events.linkDel && events.linkDel(formState);
    }
    let popup = L.popup()
        .setLatLng(latlng)
        .setContent(dom)
    popup.openOn(map);
    formState.popupOpen = true;
    popup.on('remove', () => {
        formState.popupOpen = false;
    })
    POPUP = popup;
}
//! 删除信息窗的方法
export const removePopup = (map) => {
    map.removeLayer(POPUP);
}

三、具体使用

template

  • layerListSelect 用于选择图层的组件,layer就是选中图层对象;layerListClick 切换时的事件
  • editMarker 组件表单弹窗,editSubmit表单确定事件,通过ref实例显示表单 ref.show(form)
<template>
  <div class="LMap">
    <layerListSelect :list="layerList" :active.sync="layer" @click="layerListClick"></layerListSelect>

    <div class="LMap-actions">
      <div class="LMap-actions-add" @click="clickAddPoint">
        {{ isEdit ? "点击地图添加点位" : "添加点" }}
      </div>
    </div>

    <div class="container" id="container" ref="container">

    </div>

    <editMarker @editSubmit="editSubmit" ref="RefEditMarker"></editMarker>
  </div>
</template>

script

  • data 变量用途

    • map 存放地图实例
    • layerList 天地图服务地址数组
    • layer 选中服务地址
    • isEdit 点击添加按钮 是否的判断
    • markList 存放marker点位对象数组
    • defaultMarkList 地图上默认点位的表单数组
  • mounted 初始化地图

    初始化地图,添加地图事件,设置地图图层,设置地图默认点位

  • layerListClicklayerListSelect 组件的点击事件

    用来切换图层,每次切换吧存放marker数组清空,克隆一份重新渲染点位

  • clickAddPoint 添加点位按钮,更改isEdit

    • mounted中的事件对象判断该值是否触发事件
  • mounted生命周期中的 events.click

    • 事件中的 This.$refs.RefEditMarker.show(formState) 打开表单组件
    • 通过formState type 判断新增或编辑
  • editSubmit 表单组件确认事件,返回formState表单对象

    • 通过表单对象 type 判断新增|编辑
    • 修改 则调用 editMarkerMethod.edit(markFormState,this.markList) 更改该表单对象timeIdmarkList中的每一项timeId相同的formState
    • 更改后调用this.showPopup方法
      • 该方法 有两个事件名函数,用来给信息窗中的编辑 删除添加事件
      • linkSet 点击信息窗的 编辑 触发该函数
      • linkDel 点击信息窗的 删除 触发该函数
      • 事件添加完毕 重新调用addPopup 更新信息窗表单内容
    • 如果是新增则调用editMarkerMethod.add(markFormState, this.map, this.markList, event);
      • 传入formState表单对象,map地图实例,markList存放marker的数组,event事件名对象
        • event.click marker点击事件,显示信息窗调用this.showPopup(e.target.formState);
        • event.dblclick 双击可编辑点位formState 后续放弃了改为 信息窗内的按钮操作
        • event.move marker拖动事件,每次移动更新该点位的坐标对象
        • event.dragend marker拖动结束事件,拖动后显示新的位置信息窗,先判断该信息窗是否打开
<script>
import {initMap, setLayer, layerList, MapEvents, editMarkerMethod, addPopup,removePopup} from './tools/methods';
import layerListSelect from './components/layerList/layerList.vue';
import editMarker from './components/editMarker/editMarker.vue';
import deepClone from "./tools/deepClone";

export default {
  data() {
    return {
      map: null,
      layerList,
      layer: null,
      isEdit: false,
      markList: [],
      defaultMarkList:[
        {
          latlng:{
            lat:41.44272637767212,
            lng:104.150390625
          },
          name: '名字1',
          region: 'region',
          hotType: '啊我的娃',
          timeId:1,
        },
        {
          latlng:{
            lat:41.84272637767212,
            lng:124.150390625
          },
          name: '名字1',
          region: 'region',
          hotType: '啊我的娃',
          timeId:2,
        },
        {
          latlng:{
            lat:42.44272637767212,
            lng:104.150390625
          },
          name: '名字1',
          region: 'region',
          hotType: '啊我的娃',
          timeId:3,
        }
      ]
    }
  },
  components: {
    layerListSelect,
    editMarker
  },
  mounted() {
    this.map = initMap(this.$refs.container);
    setLayer(this.map, this.layer);

    let This = this;
    const events = {
      click(e) {
        if(!This.isEdit) return;
        let formState = {
          type: '添加',
          latlng: e.latlng,
          timeId:+new Date()
        }
        This.$refs.RefEditMarker.show(formState);
      },
      zoom(e) {
        console.log('zoom',e)
      }
    }
    MapEvents(this.map, events);

    this.addDefaultMarkList();
  },
  methods: {
    layerListClick() {
      setLayer(this.map, this.layer);
      //! 每次切换图层后 重新回显内容
      let deepArray = deepClone(this.markList);
      this.markList.length = 0;
      deepArray.map((item)=>{
        this.editSubmit(item.formState);
      })
    },
    clickAddPoint() {
      this.isEdit = !this.isEdit;
    },

    showPopup(formState){
      let linkSet = (formState) =>{
        formState.type = "修改";
        this.$refs.RefEditMarker.show(formState);
      }
      let linkDel = (formState) =>{
        let findK = null;
        let findItem = this.markList.find((item,index)=>{
          if(item.formState.timeId === formState.timeId){
            findK = index;
            return item;
          }
        })
        findItem && findItem.remove && findItem.remove();
        this.map.markers.removeLayer(findItem);
        this.markList.splice(findK,1);
        removePopup(this.map);
      }
      addPopup(this.map,formState,{click:linkSet,linkDel});
    },

    editSubmit(markFormState) {
      if(markFormState.type === '修改'){
        editMarkerMethod.edit(markFormState,this.markList)
        this.showPopup(markFormState);
      }
      if (markFormState.type === '添加' || !markFormState.type) {
        let event = {
          click: (e) => {
            this.showPopup(e.target.formState);
          },
          dblclick:(e)=>{
            let formState = e.target.formState;
            formState.type = "修改";
            this.$refs.RefEditMarker.show(formState);
          },
          move: (e) => {
            if (e.latlng) {
              e.target.formState.latlng = e.latlng;
            }
          },
          dragend: (e) => {
            if(e.target.formState.popupOpen){
              this.showPopup(e.target.formState)
            }
          }
        }
        editMarkerMethod.add(markFormState, this.map, this.markList, event);
      }
      this.isEdit = false;
    },

    addDefaultMarkList(){
      this.defaultMarkList.map((item)=>{
        this.editSubmit(item);
      })
    }
  }
}
</script>

editMarker组件

editMarke

<template>
  <el-dialog
      :title="title"
      :visible.sync="dialogVisible"
      width="30%"
      :before-close="handleClose">

    <editMarkerForm ref="refForm"></editMarkerForm>

    <span slot="footer" class="dialog-footer">
      <el-button @click="clickClose">取 消</el-button>
      <el-button type="primary" @click="clickSubmit">确 定</el-button>
    </span>
  </el-dialog>
</template>

<script>
import editMarkerForm from "./editMarker.form.vue";
export default {
  name: "editMarker",
  components:{
    editMarkerForm
  },
  data() {
    return {
      dialogVisible: false,
      title:""
    };
  },
  methods: {
    handleClose(done) {
      this.$confirm('确认关闭?')
          .then(_ => {
            this.$refs.refForm.initFormState();
            done();
          })
          .catch(_ => {
          });
    },
    show(markForm) {
      this.dialogVisible = true;
      this.title = markForm.type;
      this.$nextTick(()=>{
        this.$refs.refForm.setFormState(markForm);
      })
    },
    clickSubmit(){
      let formData = this.$refs.refForm.toFormState();
      this.$emit("editSubmit",formData);
      this.dialogVisible = false;
    },
    clickClose(){
      this.$refs.refForm.initFormState();
      this.dialogVisible = false;
    }
  }
}
</script>

<style scoped>

</style>

editMarke.form

<template>
  <el-form :label-position="labelPosition" label-width="80px" :model="formState">
    <el-form-item label="名称">
      <el-input v-model="formState.name"></el-input>
    </el-form-item>
    <el-form-item label="活动区域">
      <el-input v-model="formState.region"></el-input>
    </el-form-item>
    <el-form-item label="活动形式">
      <el-input v-model="formState.hotType"></el-input>
    </el-form-item>
  </el-form>
</template>
<script>
import deepClone from "../../tools/deepClone";

export default {
  name: "editMarker.form",
  data() {
    return {
      labelPosition: 'right',
      formState: {
        name: '',
        region: '',
        hotType: ''
      },
    };
  },
  methods: {
    setFormState(formData) {
      let form = deepClone(formData);
      this.formState.name = form.name || '';
      this.formState.region = form.region || '';
      this.formState.hotType = form.hotType || '';
      this.formState.latlng = form.latlng;
      this.formState.type = form.type;
      this.formState.timeId = form.timeId;
    },
    initFormState() {
      this.formState.name = '';
      this.formState.region = '';
      this.formState.hotType = '';
    },
    toFormState() {
      return deepClone(this.formState);
    }
  }
}
</script>
<style scoped>

</style>

layerList组件

<template>
  <div class="LMap-layerList">
    <div class="LMap-layerList-item" @click="handleClickItem(item)" :key="index" v-for="(item,index) in list" :class="{'LMap-layerList-active':isEqual(item,active)}">
      <div class="LMap-layerList-item-name">
        {{item.name}}
      </div>
    </div>
  </div>
</template>

<script>
import isEqual from "../../tools/isEqualObject";
export default {
  name: "layerList",
  props:{
    list:{
      type:Array,
      default:function(){
        return []
      }
    },
    active:{
      type:Object,
      default:function (){
        return {}
      }
    },
    onClick:{
      type:Function,
      default:function(){
        return null
      }
    }
  },
  created(){
    this.$emit("update:active", this.list[0])
  },
  methods:{
    isEqual,
    handleClickItem(item){
      this.$emit("update:active",item)
      this['onClick'](item);
      this.$emit('click', item);
    }
  }
}
</script>

<style scoped>
.LMap-layerList{
  width: 100%;
  display: flex;
  padding: 10px 0;
}
.LMap-layerList-item{
  flex: 1;
  min-height: 50px;
  box-shadow: -1px -1px 3px #8ddb8c5a,1px 1px 3px #4298f95a;
  border-radius: 10px;
  cursor: pointer;
  border: 1px solid #eee;
}
.LMap-layerList-item+.LMap-layerList-item{
  margin-left: 10px;
}
.LMap-layerList-item-name{
  padding: 6px 12px;
  font-size: 14px;
  color: #000;
}
.LMap-layerList-active{
  box-shadow: -1px -1px 3px #8ddb8c,1px 1px 3px #4298f9;
  background-color: #4298f90a;
}
</style>