跳至主要內容

Leaflet

程序员李某某大约 12 分钟

Leaflet

介绍

特点

  • 轻量级
  • 移动端支持很好
  • 是基于 dom 方式渲染
  • 官网:www.leafletjs.com

下面是四个不同的框架的对比:

地图框架基本信息优缺点
CesiumWebGL渲染机制、二三维一体化可视化表达;经纬度坐标系、支持球体;优点:唯一开源的WebGIS三维引擎;适用于Web强三维应用场景
MapboxWebGL渲染机制、二三维一体化;三维方面存在一定争议,有人认为3D有的认为是2.5D;墨卡托坐标系,不支持球体优点:最具美感的专题地图缺点:没有球体运用于互联网场景复杂地理信息表达,追求地图可视化效果
Openlayers仅支持二维表达;不限制坐标系;优点:二维GIS功能最丰富全面缺点:地图样式简单,难以定制高颜值的可视化效果适用于传统地理信息强GIS的二维数据Web维护和展示
LeafletCanvas渲染机制;仅支持二维表达;墨卡托投影;优点:入手简单缺点:不支持Webgl渲染性能有瓶颈适用于轻量级简单地理信息主题可视化

核心方法

  • 图层操作:
    • addLayer:添加图层
    • removeLayer:移除图层
  • 获取地图状态:
    • getCenter:获取中心点
    • getZoom:获取缩放级别
  • 改变地图状态:
    • settCenter:设置中心点
    • setZoom:设置缩放级别
  • 创建marker:
    • marker:创建marker
  • 创建dom元素:
    • divIcon:创建dom元素
  • 视图操作:
    • fitBounds:调整视图
    • flyTo: 调整视图

核心属性

  • 中心点:center
  • 缩放级别:zoom
  • 坐标系:crs
  • 容器:containerID

事件

  • click:点击
  • dblclick:双击
  • zoomstart:开始缩放
  • zoomend:结束缩放
  • move:拖拽地图
  • moveend:结束拖拽
  • load:地图加载完成
  • mouseover:鼠标悬浮
  • contextmenu:右键菜单

其他

  • 创建GeoJSON图层:L.geoJSON()
  • 创建栅格图层:L.tileLayer()
  • 创建marker:L.marker()
  • 创建div元素:L.divIcon()
  • 绑定弹窗:bindPopup()

引入

原生页面

官网下载 leafletjs.com 下载,保留 leaflet.css 和 leaflet.js 文件即可

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 引入leaflet -->
    <link rel="stylesheet" href="./leaflet.css" />
    <script src="./leaflet.js"></script>
    <!-- 样式 -->
    <style>
        #map {
            width: 100%;
            height: 100vh;
            position: absolute;
            left: 0;
            top: 0;
        }
    </style>
</head>

<body>
    <!-- 地图容器 -->
    <div id="map"></div>
    <script>
        // 创建地图
        var map = L.map('map', {
            // 中心点:纬度在前,不同于其他框架
            center: [29.4234234, 120.646456],
            zoom: 8,
            // 可以支持2个坐标系,分别是web墨卡托和wgs84,EPSG4326是wgs84坐标系的代码,如果是墨卡托投影则不需要填写这里
            crs: L.CRS.EPSG4326,
        });
    </script>
</body>

</html>

工程化框架

npm install leaflet --save

<template>
    <div id="map"></div>
</template>
<script setup>
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import { onMounted } from "vue";
onMounted(() => {
    const map = L.map("map", {
        center: [29.2342342, 120.45345],
        zoom: 9,
        crs: L.CRS.EPSG4326,
    });
});
</script>
<style scoped>
#map {
    position: absolute;
    width: 100%;
    height: 100vh;
    left: 0;
    top: 0;
}
</style>

底图渲染

在国内,基础底图的选择相对来说很少

  • 除了使用一些在线的国外的图源(bing 地图、mapbox 官方图层)
  • 90%的场景都需要使用天地图

天地图: 天地图是我们国家地理信息公共服务平台,平台提供了包含矢量、影像、地形、以及三维等多种类型的服务。我们可以调用其中的某些服务来加载影像图,因为天地图是官方的,是国家标准,因此天地图的影像图和矢量地图被用作大多数 GIS 项目的底图 天地图数据服务官网:http://lbs.tianditu.gov.cn/server/MapService.htmlopen in new window

天地图提供了两种访问方式,一种是 wmts 方式,一种是 xyz 的访问方式,两种方式的基础 url 如下:

无论使用哪一种 url 我们都必须使用 L.tileLayer 这个方法来加载,因为他们都属于瓦片 图层

const TDT_TOKEN = tdt_token;
//xyz 方式
const tdt_url =
    "https://t0.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=";
const layer = L.tileLayer(tdt_url + TDT_TOKEN, {
    zoomOffset: 1, //缩放偏移,调整 url 里的层级,xyz 方式不用写,但是wmts方式需要写
    tileSize: 256, //地图瓦片的尺寸
    maxZoom: 18, //最大缩放层级
});

如果我们需要开发移动端相关的 app,那么使用天地图矢量风格的地图作为底图可能更加合适,因此我们只要把 url 里的 img_c 换成 vec_c即可

最后我们在把这个瓦片图层添加到地图上 layer.addTo(map),当然你也可以写 map.addLayer(layer)。两者的效果是一样的。

对于 wmts 方式的加载大家要格外的注意,一定要引入 leaflet 的 css 文件,不引入的 后果就是地图图片是错乱的。另外如果是采用 wgs84 坐标系,一定要记得打开 zoomOffset 参数

图层操作

矢量图层

引入geojson

var map = L.map('map', {
    center: [29.4234234, 120.646456],
    zoom: 8,
    // 可以支持2个坐标系,分别是web墨卡托和wgs84
    crs: L.CRS.EPSG4326,
});
// 加载杭州数据
const layerH = L.geoJson(hangzhou);
map.addLayer(layerH);
// 自动调整缩放级别和中心点,getBounds( )意为获取图层的边界范围,而 fitBounds( )意为飞行到适配到这个边界范围
map.fitBounds(layerH.getBounds());

填充样式

OptionTypeDefaultDescription
strokeBooleantrue是否沿路径绘制边框。将其设置为 "false "可禁用多边形或圆形的边框。
colorString'#3388ff'描边颜色
weightNumber3描边宽度(以像素为单位)
opacityNumber1.0描边不透明度
lineCapString'round'一个字符串,用于定义笔画的末端使用的形状open in new window
lineJoinString'round'一个字符串,用于定义笔画边角的形状open in new window
dashArrayStringnull定义笔画 虚线模式open in new window的字符串。在 某些旧浏览器open in new window 中由 Canvasopen in new window 驱动的图层上不起作用
dashOffsetStringnullA string that defines the distance into the dash pattern to start the dashopen in new window. Doesn't work on Canvasopen in new window-powered layers in some old browsersopen in new window.
fillBooleandepends是否用颜色填充路径。将其设置为 'false' 以禁用多边形或圆上的填充。
fillColorString*填充颜色。默认为 coloropen in new window 选项的值
fillOpacityNumber0.2填充不透明度
fillRuleString'evenodd'A string that defines how the inside of a shapeopen in new window is determined.
bubblingMouseEventsBooleantrueWhen true, a mouse event on this path will trigger the same event on the map (unless L.DomEvent.stopPropagationopen in new window is used).
rendererRenderer``Use this specific instance of Rendereropen in new window for this path. Takes precedence over the map's default rendereropen in new window.
classNameStringnull在元素上设置的自定义类名。仅适用于 SVG 渲染器
const myStyle = {
    fillColor: "yellow", //填充颜色
    weight: 3, //线条颜色
    fillOpacity: 1, //填充透明度
};
const layer = L.geoJson(hangzhou, {
    style: myStyle,
});
layer.addTo(map);
map.fitBounds(layer.getBounds())

还可以用回调函数方式配置

const layer = L.geoJson(hangzhou, {
  style: function (feature) {
    //这里的feature是框架帮助我们获取到的,可以任意操作
    //这个方法的返回值必须是一个样式对象,像上面的myStyle一样
    const prop = feature.properties.name;
    if (prop === "淳安县") {
      return {
        fillColor: "blue",
        weight: 3,
        fillOpacity: 1,
      };
    } else if (prop === "建德市") {
      return {
        fillColor: "red",
        weight: 3,
        fillOpacity: 1,
      };
    } else if (prop === "富阳区") {
      return {
        fillColor: "green",
        weight: 3,
        fillOpacity: 1,
      };
    } else {
      return {
        fillColor: "yellow",
        weight: 3,
        fillOpacity: 1,
      };
    }
  },
});

事件样式

const myStyle = {
    fillColor: "yellow",
    weight: 3,
    fillOpacity: 1,
};
const layer = L.geoJson(hangzhou, {
    style: myStyle,
    onEachFeature: (feature, layer) => {
        // 绑定点击事件
        layer.on({ click: highLight });
    },
});
layer.addTo(map);
map.fitBounds(layer.getBounds())


function highLight(e) {
    layer.setStyle(myStyle);
    var newLayer = e.target;
    newLayer.setStyle({
        fillColor: "red",
    });
    // 置于顶层
    newLayer.bringToFront();
}

栅格图层

对于栅格图层的操作要比矢量图层简单的多

只需要确定栅格数据的边界范围就可以了

加载非常的简单,将图像的经纬度边界作为 imageOverlay 的参数传入即可。这个边界 可以通过一些软件生成空间参考而获得。

// 绍兴市的边界
// const bound = [119.889067, 29.225091, 121.231536, 30.286319];
const imageUrl = "./images/shaoxing.jpg",
    imageBounds = [
        [29.225091, 119.889067],
        [30.286319, 121.231536],
    ];
L.imageOverlay(imageUrl, imageBounds).addTo(map);

类似的需求还有一些没有 坐标系信息的手工绘图,例如下面这样的景区旅游地图,有的时候需要将它与真实的地图做重合展示。也是用上述方式。

Marker

默认标记

L.marker([29.394863, 120.345332]).addTo(map);

显示的是默认的标记图标,如果从官网下载的进行引入,这个图标在images文件夹内,除了 leaflet.css 和 leaflet.js 文件外,还需要images这个文件夹

自定义图标

const myIcon = L.icon({
    iconUrl: "./icons/a.png", //图标地址
    //iconSize 表示图标的尺寸大小,单位为像素,数组中第一项表示宽度,第二项表示高度
    iconSize: [50, 50],
});
// 第二个参数为自定义的图标
L.marker([29.4755343, 120.40342], {
    icon: myIcon
}).addTo(map);

DOM标记

leaflet 的属性配置里面可没有可以配置文字的地方,所以我 们就需要用到 dom 元素叠加在地图上的操作

marker 里面的 icon 也可以是 dom 元素,因此我们就实现了在地图上任意位置展示文 字的操作。沿着这个思路我们还可以实现气泡图、散点图等跟 dom 元素相关的操作

const div ="<div style='width:200px'>" + "上城区" + "</div>";

const divIcon = L.divIcon({ className: "test", html: div });
L.marker([120.219068, 30.288987].reverse(), {
    icon: divIcon,
}).addTo(map);

根据geojson中的数据渲染

const layer = L.geoJson(hangzhou, {
    onEachFeature: (feature, layer) => {
        const div = "<div style='width:200px'>" + feature.properties.name + "</div>";
        const divIcon = L.divIcon({ className: "test", html: div });
        L.marker(feature.properties.centroid.reverse(), {
            icon: divIcon,
        }).addTo(map)
    },
});
layer.addTo(map);

分类图标

const poi = [{
    name: "希望小学", lon: "124.789412", lat: "47.347325", type: "school"
}, {
    name: "蜜雪冰城", lon: "103.007012", lat: "39.53535", type: "school"
}, {
    name: "天猫超市", lon: "117.872028", lat: "27.100653", type: "market"
}, {
    name: "如家酒店", lon: "124.642233", lat: "48.042886", type: "market"
}, {
    name: "人民医院", lon: "87.038159", lat: "31.42261", type: "hospital"
}, {
    name: "和平饭店", lon: "117.038159", lat: "31.62261", type: "school"
}, {
    name: "银泰城", lon: "121.038159", lat: "29.42261", type: "market"
}, {
    name: "海底捞", lon: "111.038159", lat: "28.42261", type: "school"
},
];

function addPOI(data) {
    data.forEach((d) => {
        let url = "";
        if (d.type === "market") {
            url = "./icons/a.png";
        } else if (d.type === "school") {
            url = "./icons/b.png";
        } else {
            url = "./icons/c.png";
        }
        const myIcon = L.icon({
            iconUrl: url,
            iconSize: [30, 30],
        });
        L.marker([parseFloat(d.lat), parseFloat(d.lon)], {
            icon: myIcon,
        }).addTo(map);
    });
}
addPOI(poi);

交互及事件监听

图层点击弹窗

layer.on({
    click: (e) => {
        console.log(e);
        L.popup()
            .setLatLng([e.latlng.lat, e.latlng.lng])
            .setContent('点这里')
            .openOn(map);
    }
});

其他事件

  • click:点击
  • dblclick:双击
  • zoomstart:开始缩放
  • zoomend:结束缩放
  • move:拖拽地图
  • moveend:结束拖拽
  • load:地图加载完成
  • mouseover:鼠标悬浮
  • contextmenu:右键菜单

geojson绑定

const layer = L.geoJson(hangzhou, {
    onEachFeature: (f, l) => {
        //f 表示每一个要素,l 表示整个图层
        l.on({
            //监听图层的点击事件
            click: (e) => {
                //e.target.feature 表示当前鼠标点击后获取到的要素
                console.log(e.target);
                var popup = L.popup()
                    .setLatLng([e.latlng.lat, e.latlng.lng])
                    .setContent(e.target.feature.properties.name)
                    .openOn(map);
            },
        });
    },
});
layer.addTo(map);
map.fitBounds(layer.getBounds());

图标绑定事件

const poi = [{
    name: "希望小学", lon: "124.789412", lat: "47.347325", type: "school"
}, {
    name: "蜜雪冰城", lon: "103.007012", lat: "39.53535", type: "school"
}, {
    name: "天猫超市", lon: "117.872028", lat: "27.100653", type: "market"
}, {
    name: "如家酒店", lon: "124.642233", lat: "48.042886", type: "market"
}, {
    name: "人民医院", lon: "87.038159", lat: "31.42261", type: "hospital"
}, {
    name: "和平饭店", lon: "117.038159", lat: "31.62261", type: "school"
}, {
    name: "银泰城", lon: "121.038159", lat: "29.42261", type: "market"
}, {
    name: "海底捞", lon: "111.038159", lat: "28.42261", type: "school"
},
];

function addPOI(data) {
    data.forEach((d) => {
        let url = "";
        if (d.type === "market") {
            url = "./icons/a.png";
        } else if (d.type === "school") {
            url = "./icons/b.png";
        } else {
            url = "./icons/c.png";
        }
        const myIcon = L.icon({
            iconUrl: url,
            iconSize: [30, 30],
        });
        const marker = L.marker([parseFloat(d.lat), parseFloat(d.lon)], {
            icon: myIcon,
        }).addTo(map);

        // 绑定事件
        marker.bindPopup(`<b>${d.name}</b><br>${d.type}`);
    });
}
addPOI(poi);

图标绑定鼠标滑过事件

const marker = L.marker([29.225091, 119.889067], {
            icon: myIcon,
        }).addTo(map);

        // 绑定鼠标滑过事件
        marker.bindTooltip(`
<table>
<caption>示例表格</caption>
<thead>
    <tr>
        <th>名称</th>
        <th>类型</th>
        <th>经度</th>
        <th>纬度</th>
    </tr>
</thead>
<tbody>
    <tr>
        <td>希望小学</td>
        <td>school</td>
        <td>124.789412</td>
        <td>47.347325</td>
    </tr>
    <tr>
        <td>蜜雪冰城</td>
        <td>school</td>
        <td>103.007012</td>
        <td>39.53535</td>
    </tr>
    <tr>
        <td>天猫超市</td>
        <td>market</td>
        <td>117.872028</td>
        <td>27.100653</td>
    </tr>
    <tr>
        <td>如家酒店</td>
        <td>market</td>
        <td>124.642233</td>
        <td>48.042886</td>
    </tr>
    <tr>
        <td>人民医院</td>
        <td>hospital</td>
        <td>87.038159</td>
        <td>31.42261</td>
    </tr>
    <tr>
        <td>和平饭店</td>
        <td>school</td>
        <td>117.038159</td>
        <td>31.62261</td>
    </tr>
    <tr>
        <td>银泰城</td>
        <td>market</td>
        <td>121.038159</td>
        <td>29.42261</td>
    </tr>
    <tr>
        <td>海底捞</td>
        <td>school</td>
        <td>111.038159</td>
        <td>28.42261</td>
    </tr>
</tbody>
</table>
        `, {
            permanent: false, // 是否永久显示 tooltip
            direction: 'top', // tooltip 显示方向
            offset: L.point(0, -20) // tooltip 相对于标记的偏移量
        });

图形绘制与测量

引入leaflet-draw

  • 原生页面在github下载,引入leaflet.draw.css,leaflet.draw.js
  • 工程化项目通过npm instal leaflet-draw引入

首先要创建一个空的 featureGroup 用于保存我们后续绘制的图形要素,并把它事 先添加在地图上。随后创建一个 draw 的类,插件会帮助我们在 leaflet 的 control 配置项之 下添加一个 draw 的类。这个 draw 类中你可以进行相关配置。例如是否可以画点、线、面、 矩形,是否可以编辑修改删除等等。然后就是对用户的绘制进行监听,每当用户开始绘制之 后就可以通过监听的回调函数获取到用户所绘制的内容。这些内容都在 e.layer 中,然后把 这个 e.layer 添加到我们刚才准备的空的 featureGroup 中即可完成绘制。插件还为我们提供 了一个绘制结束的监听,你可以在其中做些其他事情,比如改变绘制图形的样式,以及给一些提示语等等。顺便我想测量大家也能够做到了,即通过使用绘制结束后的监听,计算绘制 的面积和长度即可。另外也可以仿照这个例子使用插件 measure 来完成。

//创建一个空的要素集合用于保存将来绘制的图层
let editableLayers = new L.FeatureGroup();
map.addLayer(editableLayers);
//添加绘制组件
let draw = new L.Control.Draw({
    position: "topleft",
    draw: { polyline: true, polygon: true, circle: true },
    //可以传入图层作为可编辑的图层
    edit: {
        featureGroup: editableLayers,
        remvove: false,
    },
}).addTo(map);
map.on(L.Draw.Event.CREATED, (e) => {
    //开始创建绘画的监听,在这里可以得到你绘制的图形并且添加到地图上
    //你可以通过 e.layer 来获取到你画的图形
    editableLayers.addLayer(e.layer);
});
map.on(L.Draw.Event.DRAWSTOP, (e) => {
    //这里是绘制结束的监听,你也可以在这里拿到绘制的数据并作其他操作
});
上次编辑于:
贡献者: 李元昊