Leaflet
Leaflet
介绍
特点
- 轻量级
- 移动端支持很好
- 是基于 dom 方式渲染
- 官网:www.leafletjs.com
下面是四个不同的框架的对比:
| 地图框架 | 基本信息 | 优缺点 |
|---|---|---|
| Cesium | WebGL渲染机制、二三维一体化可视化表达;经纬度坐标系、支持球体; | 优点:唯一开源的WebGIS三维引擎;适用于Web强三维应用场景 |
| Mapbox | WebGL渲染机制、二三维一体化;三维方面存在一定争议,有人认为3D有的认为是2.5D;墨卡托坐标系,不支持球体 | 优点:最具美感的专题地图缺点:没有球体运用于互联网场景复杂地理信息表达,追求地图可视化效果 |
| Openlayers | 仅支持二维表达;不限制坐标系; | 优点:二维GIS功能最丰富全面缺点:地图样式简单,难以定制高颜值的可视化效果适用于传统地理信息强GIS的二维数据Web维护和展示 |
| Leaflet | Canvas渲染机制;仅支持二维表达;墨卡托投影; | 优点:入手简单缺点:不支持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.html
天地图提供了两种访问方式,一种是 wmts 方式,一种是 xyz 的访问方式,两种方式的基础 url 如下:
- xyz:"https://t0.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk="
- wmts:"http://t0.tianditu.gov.cn/img_c/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk="
无论使用哪一种 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());
填充样式
| Option | Type | Default | Description |
|---|---|---|---|
stroke | Boolean | true | 是否沿路径绘制边框。将其设置为 "false "可禁用多边形或圆形的边框。 |
color | String | '#3388ff' | 描边颜色 |
weight | Number | 3 | 描边宽度(以像素为单位) |
opacity | Number | 1.0 | 描边不透明度 |
lineCap | String | 'round' | 一个字符串,用于定义笔画的末端使用的形状 |
lineJoin | String | 'round' | 一个字符串,用于定义笔画边角的形状 |
dashArray | String | null | 定义笔画 虚线模式的字符串。在 某些旧浏览器 中由 Canvas 驱动的图层上不起作用 |
dashOffset | String | null | A string that defines the distance into the dash pattern to start the dash. Doesn't work on Canvas-powered layers in some old browsers. |
fill | Boolean | depends | 是否用颜色填充路径。将其设置为 'false' 以禁用多边形或圆上的填充。 |
fillColor | String | * | 填充颜色。默认为 color 选项的值 |
fillOpacity | Number | 0.2 | 填充不透明度 |
fillRule | String | 'evenodd' | A string that defines how the inside of a shape is determined. |
bubblingMouseEvents | Boolean | true | When true, a mouse event on this path will trigger the same event on the map (unless L.DomEvent.stopPropagation is used). |
renderer | Renderer | `` | Use this specific instance of Renderer for this path. Takes precedence over the map's default renderer. |
className | String | null | 在元素上设置的自定义类名。仅适用于 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) => {
//这里是绘制结束的监听,你也可以在这里拿到绘制的数据并作其他操作
});
