leaflet初体验

487 阅读5分钟

引言

在地理信息可视化与交互应用日益普及的今天,Leaflet 作为一款轻量级、高性能且开源的 JavaScript 地图库,受到了广大前端开发者的青睐。它提供了强大的地图绘制、交互功能,并且具有良好的扩展性和兼容性,能够轻松满足各类 Web 地图应用的需求。本文将深入探讨 Leaflet 地图的核心技术,通过实际案例和代码示例,帮助前端开发者快速掌握其应用技巧。

一、安装

npm install leaflet

二、页面引入

main.js

import * as leaflet from 'leaflet'
import 'leaflet/dist/leaflet.css'

Vue.prototype.$leaflet = leaflet

三、创建地图

<template>
  <div id="map-container"></div>
</template>

四、初始化地图

initMap() {
      //定义图层样式
      const layer = this.$leaflet.tileLayer(
        'http://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}'
      )
      // 创建地图
      this.leafletMap = this.$leaflet.map('map-container', {
        center: [28.19854, 112.8347],
        zoom: 10,
        minZoom: 3,
        maxZoom: 14,
        // zoomSnap: 1,
        attributionControl: true, // 是否将 attribution 版权控件添加到地图中
        zoomControl: true, // 是否将 zoom 缩放控件添加到地图中
        crs: this.$leaflet.CRS.EPSG3857, // 该地图使用的坐标系。如果你不确定坐标系这是什么意思,请不要更改它。
        keyboard: true, // 地图是否获得焦点,并且允许用户通过键盘和 +/- 来进行浏览地图。
        keyboardPanDelta: 80, // 按下方向键时,平移的像素数量。
        scrollWheelZoom: 'center', // 地图是否允许通过使用鼠标滚轮进行缩放。如果通过'center',不管鼠标在哪里,都将会放大到视图的中心。
        layers: [layer] // 图层
      })
    },

image.png

瓦片图层

高德矢量图层:http://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}

高德影像图层:http://webst0{1-4}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}

百度地图:http://online{0-3}.map.bdimg.com/onlinelabel/?qt=tile&x={x}&y={y}&z={z}&styles=pl&scaler=1&p=1

OpenStreetMap:https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png

其中OpenStreetMap瓦片需要添加跨域,否则访问不了:

const layer = this.$leaflet.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        crossOrigin: true
      })
  • 添加图层控制
      const layer = this.$leaflet.tileLayer(
        'http://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}'
      )
      const gaodeSatellite = this.$leaflet.tileLayer(
        'http://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}'
      )
      // 添加图层控制
      const baseMaps = {
        高德矢量: layer,
        高德影像: gaodeSatellite
      }
      this.$leaflet.control.layers(baseMaps).addTo(this.leafletMap)

image.png

image.png

五、地图上绘制点、线、多边形、弹框

1、描点

    // 描点
    addMarker() {
      this.marker = this.$leaflet
        .marker([28.19854, 112.8347], {
          icon: this.myIcon
        })
        .addTo(this.leafletMap)
    },

2、画线

// 画线
    addPolyline() {
      const points = [
        [28.19854, 112.8347],
        [28.29854, 112.4347],
        [28.39854, 112.6347]
      ]
      this.$leaflet.polyline(points, { color: 'red' }).addTo(this.leafletMap)
    },

3、画圆

// 画圆
    addCircle() {
      const popup = this.$leaflet.popup().setContent('<p style="color:green;">欢迎你</p>')
      this.$leaflet
        .circle([28.2908, 113.26625], 5000, {
          color: 'red',
          fillColor: '#f03',
          fillOpacity: 0.5
        })
        .addTo(this.leafletMap)
        .bindPopup(popup)
        .openPopup()
    },

4、画多边形

    // 画多边形
    addPolygon() {
      const points = [        [28.19854, 113.8347],
        [28.29854, 113.4347],
        [28.39854, 113.6347]
      ]
      this.$leaflet
        .polygon(points, { color: '#aa0000', fillColor: '#ff15c9', weight: 1 })
        .addTo(this.leafletMap)
    },
    

5、pm绘制

// 安装依赖
npm install leaflet.pm

// main.js引入
import 'leaflet.pm'
import 'leaflet.pm/dist/leaflet.pm.css'
<a-radio-group v-model="drawerType" @change="drawStart">
  <a-radio-button value="Circle">绘制圆形</a-radio-button>
  <a-radio-button value="Rectangle">绘制矩形</a-radio-button>
  <a-radio-button value="Polygon">绘制多边形</a-radio-button>
  <a-radio-button class="red" v-if="drawerType" value=""> 取消绘制</a-radio-button>
</a-radio-group>

drawStart() {
      if (this.drawerType) this.handleStart()
      else this.handleCancel()
    },
    handleStart() {
      this.leafletMap.pm.enableDraw(this.drawerType, {
        snappable: false
      })
      this.getlatLngs(this.drawerType)
    },
    getlatLngs(drawerType) {
      // pm:drawstart 开始第一个点的时候调用
      // pm:drawend  禁止绘制时调用
      // pm:create  创建完成时调用
      // pm:remove  移除完成时调用
      this.leafletMap.on('pm:drawstart', (e) => {
        console.log(e, '开始第一个点的时候调用')
      })
      this.leafletMap.on('pm:drawend', (e) => {
        console.log(e, '禁止绘制')
      })
      this.leafletMap.on('pm:create', (e) => {
        console.log(e, '绘制完成时调用', drawerType)
        // 获取被选中的设备
        const polygon = e.layer
        const bounds = polygon.getBounds()
        let center = null // 圆心坐标
        let radius = null // 单位:米
        let fillMarket = this.deviceListData.filter((item) => bounds.contains(item.latlng))
        console.log('图形包含选中设备:', fillMarket)
        if (fillMarket.length) {
          // 绘制多边形
          if (drawerType === 'Polygon') {
            console.log('Polygon===')
            fillMarket = []
            this.deviceListData.forEach((item) => {
              if (this.isPointInPolygon(this.$leaflet.latLng(item.latlng), polygon)) {
                fillMarket.push(item)
              }
            })
          } else if (drawerType === 'Circle') {
            // 绘制圆形
            console.log('Circle===')
            fillMarket = []
            center = polygon.getLatLng() // 圆心坐标
            radius = polygon.getRadius() // 半径
            // 筛选在圆形范围内的 Marker
            fillMarket = this.deviceListData.filter(
              (item) => center.distanceTo(item.latlng) <= radius
            )
          }
          console.log('fillMarket===', fillMarket)
          if (fillMarket?.length > 0) this.handleSelected(fillMarket)
        }
        // 循环markersLayer图层上所有marker
        // this.markersLayer.eachLayer((marker) => {
        //   // 判断marker在绘制范围内
        //   if (bounds.contains(marker.getLatLng())) {
        //     const polygonFlag =
        //       drawerType === 'Polygon' && !this.isPointInPolygon(marker.getLatLng(), polygon)
        //     const circleFlag =
        //       drawerType === 'Circle' && center.distanceTo(marker.getLatLng()) > radius
        //     if (polygonFlag || circleFlag) return
        //     const { alt } = marker.options
        //     marker.setIcon(
        //       this.$leaflet.icon({
        //         iconUrl: this.transIcons({ devType: Number(alt.split('-')[1]), status: 99 }),
        //         iconSize: [40, 40], // 图标大小,单位(px)
        //         popupAnchor: [-20, 0], // popup相对于锚点中心的坐标
        //         tooltipAnchor: [0, -20] // tooltip相对于锚点中心的坐标
        //       })
        //     )
        //   }
        // })
        this.cancelDraw()
      })
      this.leafletMap.on('pm:remove', (e) => {
        console.log(e, '移除绘制时调用')
      })
    },
    // 取消绘制
    handleCancel() {
      this.leafletMap.pm.disableDraw(this.drawerType)
      // 移除当前绘制图像
      this.leafletMap.eachLayer((layer) => {
        const { _path: path } = layer
        if (path) layer.remove()
      })
      this.drawerType = null
    },

image.png

image.png

image.png

6、绘制弹框


    // 绘制弹框
    addPopup() {
      const popup = this.$leaflet
        .popup()
        .setContent('<p style="color:green;">我是hzz!<br />我在长沙</p>')
      this.marker.bindPopup(popup)
    },

7、定义图片覆盖层

const bounds = [
        [0, 0],
        [600, 300]
      ]
      this.$leaflet.imageOverlay(imgUrl, bounds).addTo(this.map)
      this.map.fitBounds(bounds)

8、设置语言

this.leafletMap.pm.setLang('en') // zh

image.png

9、绘制区域颜色

    addAreaColor() {
      // 区域样式
      const style = {
        color: '#55ff7f', // 边框颜色
        weight: 3, // 边框粗细
        opacity: 0.5, // 透明度
        fillColor: '#55ff7f', // 区域填充颜色
        fillOpacity: 0.2 // 区域填充颜色的透明
      }
      const s = this.$leaflet.geoJSON(changsha, { style }).addTo(this.leafletMap)
      console.log('addAreaColor===', s)
    },

10、热力图

  • 下载插件
npm install heatmap.js
  • 页面引入
import HeatmapOverlay from 'heatmap.js/plugins/leaflet-heatmap'
  • 应用
addHeartLayer() {
      const testData = {
        max: 8, // 最大值
        data: [
          {
            lat: 28.39854,
            lng: 113.3347,
            count: 5
          },
          {
            lat: 28.38854,
            lng: 113.3447,
            count: 8
          },
          {
            lat: 28.39654,
            lng: 113.3647,
            count: 2
          },
          {
            lat: 28.41854,
            lng: 113.3447,
            count: 7
          },
          {
            lat: 28.52554,
            lng: 113.4247,
            count: 8
          },
          {
            lat: 28.53554,
            lng: 114.4447,
            count: 8
          },
          {
            lat: 28.55554,
            lng: 114.4447,
            count: 8
          },
          {
            lat: 28.91554,
            lng: 113.4147,
            count: 8
          },
          {
            lat: 28.88654,
            lng: 113.3647,
            count: 3
          }
        ]
      }
      // 配置
      const config = {
        radius: 0.015, // 设置每一个热力点的半径
        maxOpacity: 0.8, // 设置最大的不透明度
        minOpacity: 0, // 设置最小的不透明度
        scaleRadius: true, // 设置热力点是否平滑过渡
        useLocalExtrema: false, // 使用局部极值
        latField: 'lat', // 纬度
        lngField: 'lng', // 经度
        valueField: 'count', // 热力点的值
        gradient: {
          // 热力点颜色的变化范围
          0.99: 'rgba(255,0,0,1)',
          0.9: 'rgba(255,255,0,1)',
          0.8: 'rgba(0,255,0,1)',
          0.5: 'rgba(0,255,255,1)',
          0: 'rgba(0,0,255,1)'
        }
      }
      const heatmapLayer = new HeatmapOverlay(config)
      heatmapLayer.setData(testData)
      this.leafletMap.addLayer(heatmapLayer)
    }

效果图:

image.png

六、性能优化

  • 图层加载优化

    懒加载:对于包含多个图层的地图,可以采用懒加载策略,只在用户需要时加载特定图层,减少初始加载时间

    瓦片预加载:通过设置瓦片图层的preload属性,提前加载部分瓦片,提高地图浏览的流畅性

    const layer = this.$leaflet.tileLayer(
            'http://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}',
            {
              preload: true // 开启瓦片预加载
            }
          )
    
  • 事件管理

    合理管理地图事件,避免过多的事件监听导致性能下降

    map.on('click', handler);
    ​
    map.off('click', handler);
    
  • 地图渲染优化

    减少图层数量:避免在地图上同时显示过多图层,根据实际需求动态切换图层。

    优化图形绘制:对于复杂的图形(如多边形、折线),尽量减少顶点数量,简化图形结构,提高渲染性能。

七、常见问题与解决方案

  • 地图偏移问题

    在使用某些地图服务(如高德地图、百度地图)时,可能会出现地图坐标偏移的情况。这是因为这些地图服务使用了自己的坐标系统。解决方案是使用相应的坐标转换库(如bd09-llgcj02-ll)将坐标转换为正确的经纬度坐标。

    npm install gcj02-ll
    ​
    const { wgs84togcj02, gcj02towgs84 } = require('gcj02-ll');
    ​
    // 假设这是高德地图的坐标
    const gcj02Point = [34.0522, 118.2437];
    ​
    // 转换为 WGS-84 坐标
    const wgs84Point = gcj02towgs84(gcj02Point[0], gcj02Point[1]);
    console.log('WGS-84 坐标:', wgs84Point);
    
  • 移动端适配问题

    优化触摸交互:调整地图的触摸事件处理逻辑,使其更适合移动端操作。

    const map = L.map('map', {
        zoomControl: false, // 禁用默认的缩放控件
        dragging: false, // 禁用默认的拖动行为
        touchZoom: false, // 禁用默认的触摸缩放行为
        doubleClickZoom: false // 禁用默认的双击缩放行为
    });
    ​
    // 自定义触摸事件
    map.on('touchstart', function (e) {
        console.log('Touch start event:', e);
    });
    ​
    map.on('touchmove', function (e) {
        console.log('Touch move event:', e);
    });
    ​
    map.on('touchend', function (e) {
        console.log('Touch end event:', e);
    });
    

    响应式布局:使用 CSS 媒体查询等技术,实现地图在不同屏幕尺寸下的自适应显示。

总结

Leaflet 地图库以其简洁的 API、强大的功能和良好的扩展性,为前端开发者提供了高效的地图开发解决方案。通过掌握 Leaflet 的基础概念、核心功能、进阶应用以及性能优化和问题解决方法,开发者能够创建出功能丰富、性能优良的 Web 地图应用。在实际项目中,应根据具体需求灵活运用这些技术,并结合插件和第三方服务,不断拓展地图应用的边界。