WebGIS 开发基础之 Leaflet
GIS、Map、Layer、Feature、Geometry、Symbol、Data(Point、Polyline、Polygon)、Renderer、Scale、Project、Coordinates;
架构模式、常用平台和 SDK、二维、三维
- 地图加载(底图类型、切换)
- 地图操作(缩放、平移、定位/书签、动画)
- 图层管理(加载、移除、调整顺序)
- 要素标绘(点/聚簇、线、面,符号化/静态动态)
- 属性标注(字段可选、样式定制)
- 专题地图(点、线、面,渲染)
- 查询定位(属性查询、空间查询/周边搜索/缓冲区/面查点线面/点线查面、图属互查、综合查询)
- 信息窗口(入口、Popup、定制)
- 坐标转换(地理与投影、不同地理坐标系)
- 空间运算(长度面积测量、点取坐标、缓冲区、相交包含关系)
- 动态监控(固定点状态切换、车辆监控)
- Flat-UI - 基于 Bootstrap 的一个扁平化风格 web 开发框架。
- Leaflet - 一个为建设交互性好适用于移动设备地图,而开发的现代的、开源的 JavaScript 库。
- Esri Leaflet - 一个轻量级的工具包,基于 leaflet 利用 ArcGIS 服务。
PART 1: 地图加载(底图类型、切换) Demo 1
- 库引用
<link rel="stylesheet" type="text/css" href="./lib/Flat-UI-master/dist/css/vendor/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="./lib/Flat-UI-master/dist/css/flat-ui.min.css">
<link rel="stylesheet" href="./lib/leaflet/leaflet.css">
<script src="./lib/Flat-UI-master/dist/js/vendor/jquery.min.js"></script>
<script src="./lib/Flat-UI-master/dist/js/flat-ui.js"></script>
<script src="./lib/leaflet/leaflet.js"></script>
<script src="./js/urlTemplate.js"></script>
- 地图加载与切换
const map = L.map('mapDiv', {
// 要使用的坐标参考系统,默认的坐标参考系,互联网地图主流坐标系
crs: L.CRS.EPSG3857,
// WGS84 坐标系,GPS 默认坐标系
// crs: L.CRS.EPSG4326,
zoomControl: true,
// minZoom: 1,
attributionControl: false,
}).setView([31.626866, 104.152894], 18);
// 定位在成都北纬 N30°37′45.58″ 东经 E104°09′1.44″
let baseLayer = L.tileLayer(urlTemplate.mapbox_Image, {
// 最大视图
maxZoom: 17,
// 最小视图
minZoom: 2,
attribution:
'@lvisei © <a href="https://github.com/lvisei/leaflet-demo">leaflet-demo</a>',
}).addTo(map);
const setLayer = (ele) => {
map.removeLayer(baseLayer);
if (ele == 'mapbox_Image') {
baseLayer = L.tileLayer(urlTemplate.mapbox_Image, {
maxZoom: 17,
minZoom: 2,
}).addTo(map);
} else if (ele == 'mapbox_Vector') {
baseLayer = L.tileLayer(urlTemplate.mapbox_Vector, {
maxZoom: 17,
minZoom: 1,
}).addTo(map);
}
};
PART 1.1:基于 Demo 1 利用 H5 Geolocation API 定位到当前位置 Demo 1.1
- 库引用 如上 Demo 1
<!-- marker高亮显示库引用 -->
<link rel="stylesheet" href="./lib/leaflet.marker.highlight/leaflet.marker.highlight.css">
<script src="./lib/leaflet.marker.highlight/leaflet.marker.highlight.js"></script>
- 判断浏览器是否支持
let map;
let baseLayer;
// 使用H5 API定位 定位在当前位置
if (navigator.geolocation) {
console.log('/* 地理位置服务可用 */');
navigator.geolocation.getCurrentPosition(h5ApiSuccess, h5ApiError);
} else {
console.log('/* 地理位置服务不可用 */');
// 指定一个数据 定位在成都北纬N30°37′45.58″ 东经E104°09′1.44″
mapInit([30.626866, 104.152894]);
}
- 定位成功或失败处理方法
const h5ApiSuccess = (position) => {
// 纬度
const latitude = position.coords.latitude;
// 经度
const longitude = position.coords.longitude;
console.log('你的经度纬度分别为' + longitude + ',' + latitude + '。');
return mapInit([latitude, longitude]);
};
const h5ApiError = () => {
console.log('/* 地理位置请求失败 */');
// 指定一个数据 定位在成都北纬N30°37′45.58″ 东经E104°09′1.44″
mapInit([31.626866, 104.152894]);
};
- 成功后初始化底图
const mapInit = (LatLng) => {
map = L.map('mapDiv', {
// 要使用的坐标参考系统,默认的坐标参考系,互联网地图主流坐标系
crs: L.CRS.EPSG3857,
// WGS84 坐标系,默认坐标系
// crs: L.CRS.EPSG4326,
zoomControl: true,
// minZoom: 1,
attributionControl: true,
// 定位在当前位置
}).setView(LatLng, 18);
baseLayer = L.tileLayer(urlTemplate.mapbox_Image, {
// 最大视图
maxZoom: 17,
// 最小视图
minZoom: 2,
attribution:
'@lvisei © <a href="https://github.com/lvisei/leaflet-demo">leaflet-demo</a>',
}).addTo(map);
L.marker(LatLng, {
// 永久高亮显示
highlight: 'permanent',
}).addTo(map);
};
- 更多内容
- 更多了解 geolocation 对象,可参考 MDN Web 文档
- 更多了解使用 marker 高亮显示,可参考 leaflet.marker.highlight 插件
- 基于 Demo 1 利用 leaflet 封装好的 H5 定位 API,定位到当前位置 Demo
PART 2: 地图操作(缩放、平移、定位/书签、动画) Demo 2
-
库引用 如上 Demo 1
-
设置地图缩放到指定图层
const setZoom = () => {
// 设置地图缩放到
map.setZoom(10, {
// animate: false
});
};
- 图层往里进一个图层,放大
const setZoomIn = () => {
// 图层往里进一个图层,放大
map.zoomIn();
};
const setZoomOut = () => {
// 图层往里出一个图层,缩小
map.zoomOut();
};
- 地图平移至中心点
const panTo = () => {
// 地图平移,默认就是true,将地图平移到给定的中心。如果新的中心点在屏幕内与现有的中心点不同则产生平移动作。
map.panTo([37.91082, 128.73583], {
animate: true,
});
};
- 地图飞到中心点
const flyTo = () => {
// 点到点的抛物线动画,平移加缩放动画
map.flyTo([36.52, 120.31]);
};
注意:尽量避免 setZoom()等地图缩放方法与 flyTo、flyToBounds 一起合用,因为这两类地图操作方法都有各自的缩放值,造成动画不流畅、不能定位到目的点。
- 地图飞到边界的合适的位置
const flyToBounds = () => {
// getBounds(获取边界):返回地图视图的经纬度边界。
map.flyToBounds(polygon.getBounds());
// 飞到这个多变形区域上面,自动判断区域块的大小,合适缩放图层,将地图视图尽可能大地设定在给定的地理边界内。
};
let polygon = L.polygon(
[
[37, -109.05],
[41, -109.03],
[41, -102.05],
[37, -102.04],
],
[40.774, -74.125],
{
color: 'green',
fillColor: '#f03',
fillOpacity: 0.5,
}
).addTo(map);
// 地图上绘制一个多边形
- 地图定位到边界的合适的位置
const fitBounds = () => {
// getBounds(获取边界):返回地图视图的经纬度边界。
console.log(polygon.getBounds());
map.fitBounds(polygon.getBounds());
// 平移到一个区域上面,自动判断区域块的大小,合适缩放图层
};
let polygon = L.polygon(
[
[37, -109.05],
[41, -109.03],
[41, -102.05],
[37, -102.04],
],
[40.774, -74.125],
{
color: 'green',
fillColor: '#f03',
fillOpacity: 0.5,
}
).addTo(map);
// 地图上绘制一个多边形
PART 3: 图层管理(加载、移除、调整顺序): Demo 3
- 库引用
<link rel="stylesheet" type="text/css" href="./lib/Flat-UI-master/dist/css/vendor/bootstrap/css/bootstrap.min.css"
/>
<link rel="stylesheet" href="./lib/Flat-UI-master/dist/css/flat-ui.min.css">
<link rel="stylesheet" href="./lib/leaflet/leaflet.css">
<script src="./lib/Flat-UI-master/dist/js/vendor/jquery.min.js"></script>
<script src="./lib/Flat-UI-master/dist/js/flat-ui.js"></script>
<script src="./lib/leaflet/leaflet.js"></script>
<!-- esri-leafleat插件 -->
<script src="./lib/esri-leaflet-v2.1.2/dist/esri-leaflet.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/proj4js/2.6.2/proj4.js"></script>
<script src="./js/urlTemplate.js"></script>
- 使用 esri-leaflet 插件加载 ArcGIS 底图服务
let oMap = null;
let oLayer = [];
oMap = L.map('mapDiv', {
crs: L.CRS.EPSG4326,
zoomControl: false,
minZoom: 7,
attributionControl: false
// 定位在重庆
}).setView([29.59, 106.59], 12);
oLayer.push(L.esri.tiledMapLayer({
url: urlTemplate.SYS_IMG_MAPSERVER_PATH,
maxZoom: 17,
minZoom: 0,
// 是否浏览器在跨域的情况下使用GET请求
useCors: false,
}).addTo(oMap));
// 加载第一个底图
oLayer.push(L.esri.tiledMapLayer({
url: urlTemplate.SYS_IMG_LABEL_MAPSERVER_PATH,
maxZoom: 17,
minZoom: 0,
useCors: false,
}).addTo(oMap));
// 加载第二个底图
- 切换底图(移除及加载)
const setLayer = (layerUrls, maxZoom) => {
for (let i = 0; i < oLayer.length; i++) {
// 将图层在地图上移除
oMap.removeLayer(oLayer[i]);
}
// 制空数组
oLayer = [];
layerUrls.map((item) => {
oLayer.push(
L.esri
.tiledMapLayer({
url: item,
// 是否浏览器在跨域的情况下使用GET请求
useCors: false,
maxZoom: maxZoom,
})
.addTo(oMap)
);
});
};
不同的底图可能图层数不一样,就可能造成浏览器去请求不存在的图层,以及给用户展示出空白区域的不好体验,所以切换图层时候应注意设置最大及最小缩放值。
PART 4: 要素标绘(点、线、面,符号化/静态动态) Demo 4
-
库引用 如上 Demo 1
-
画一个圆
// 画一个 circle
const circle = L.circle([36.52, 120.31], {
// 描边色
color: 'green',
// 填充色
fillColor: '#f03',
// 透明度
fillOpacity: 0.5,
// 半径,单位米
radius: 10000
}).addTo(map);
// 绑定一个提示标签
circle.bindTooltip('我是个圆');
- Maker 及自定义 Maker
// 生成一个 maker
const marker = L.marker([36.52, 120.31]).addTo(map);
// 绑定一个提示标签
marker.bindTooltip('这是个Marker', { direction: 'left' }).openTooltip();
// 自定义一个 maker
const greenIcon = L.icon({
iconUrl: './icon/logo.png',
// size of the icon
iconSize: [300, 79],
// point from which the popup should open relative to the iconAnchor
popupAnchor: [0, -10]
});
const oMarker = L.marker([36.52, 124.31], { icon: greenIcon }).addTo(map);
// 绑定一个提示标签
oMarker.bindTooltip('这是个自定义Marker', { direction: 'left', offset: [-150, 0] });
- 画一根线
// 画一根线
const polyline = L.polyline([[45.51, -122.68], [37.77, -122.43], [34.04, -118.2]], { color: 'red' }).addTo(map);
// 飞到这个线的位置
map.fitBounds(polyline.getBounds());
- 画一个多边形
// 画一个polygon
const polygon = L.polygon([
[[37, -109.05], [41, -109.03], [41, -102.05], [37, -102.04]], // outer ring
[[37.29, -108.58], [40.71, -108.58], [40.71, -102.50], [37.29, -102.50]] // hole
], {
color: 'green',
fillColor: '#f03',
fillOpacity: 0.5
}).addTo(map);
// 绑定一个提示标签
polygon.bindTooltip('this is 个多边形');
// 飞到这个多边形的位置
map.fitBounds(polygon.getBounds());
PART 5: 信息窗口(入口、Popup、定制) Demo 5
-
库引用 如上 Demo 1
-
画一个 circle 并绑定一个 Popup
// 画一个circle
const circle = L.circle([36.92, 121.31], {
color: 'green', //描边色
fillColor: '#f03', //填充色
fillOpacity: 0.5, //透明度
radius: 10000 //半径,单位米
}).addTo(map);
// 绑定一个弹窗
circle.bindPopup('我是个圆');
- 定位一个 marker,绑定一个自定义 Popup
// 定位一个maker
const marker = L.marker([36.52, 120.31]).addTo(map);
// maker上自定义一个 popup
const html = '<p>Hello world!<br />This is a nice popup.</p>';
const popup = marker.bindPopup(html, { maxHeight: 250, maxWidth: 490, className: 'content', offset: [0, 0] }).on('popupopen', function (params) {
console.log(params)
});
- 实现动态改变 Popup 的内容
const mypop = L.popup();
map.on('click', function (e) {
mypop.setLatLng(e.latlng)
.setContent('你临幸了这个点:<br>' + e.latlng.toString())
.openOn(map);
});
PART 6: geojson 数据绘制边界(坐标转换、渲染) Demo 6
-
库引用 如上 Demo 3
-
获得 geojson 并处理数据
// 请求 geojson 并处理数据
const population = () => {
$.get('./js/geojson.json', function (response) {
const poplData = response.data;
const PolygonsCenter = response.geopoint;
drawPolygons(poplData, PolygonsCenter);
});
};
Mock 返回的数据 GeoJSON
- 绘制边界并添加图例
let oPolygon_VilPop = [];
const legend = L.control({
position: 'bottomright'
});
// 绘制边界
const drawPolygons = (poplData, PolygonsCenter) => {
for (const i in poplData) {
poplData[i].geoJson = JSON.parse(poplData[i].geoJson);
oPolygon_VilPop[i] = L.geoJSON(poplData[i].geoJson, {
style: function () {
return {
color: 'white',
// 获取边界的填充色
fillColor: getBgColor(poplData[i].population),
fillOpacity: 0.6,
weight: 3,
dashArray: '10',
};
},
})
.bindTooltip(poplData[i].villageName + '<br><br>人口' + poplData[i].population + '人', {
direction: 'top',
})
.on({
// 鼠标移动上去高亮
mouseover: highlight,
// 鼠标移出恢复原样式
mouseout: resetHighlight,
// 点击最大化
click: zoomTo,
})
.addTo(oMap);
}
// 添加图例
legend.onAdd = legendHtml;
legend.addTo(oMap);
// 定位到该界限的中心位置
oMap.flyToBounds(PolygonsCenter);
};
- 返回边界的填充色及图列的样式
const getBgColor = (d) => {
return d > 400
? '#800026'
: d > 300
? '#BD0026'
: d > 200
? '#FC4E2A'
: d > 100
? '#FD8D3C'
: d > 50
? '#FED976'
: '#FFEDA0';
};
const legendHtml = (map) => {
let div = L.DomUtil.create('div', 'legend locateVP_legend'),
grades = [0, 50, 100, 200, 400],
labels = [],
from,
to;
for (const i = 0; i < grades.length; i++) {
from = grades[i];
to = grades[i + 1];
labels.push(
'<i style="background:' + getBgColor(from + 1) + '"></i> ' + from + (to ? ' ∼ ' + to + '人' : '以上')
);
}
div.innerHTML = labels.join('<br>');
return div;
};
- 鼠标移动上去的事件、鼠标移出的事件、发生点击的事件
const highlight = (e) => {
const layer = e.target;
layer.setStyle({
weight: 6,
color: '#fff',
fillOpacity: 0.9,
dashArray: '0',
});
};
const resetHighlight = (e) => {
const layer = e.target;
layer.setStyle({
color: 'white',
weight: 3,
fillOpacity: 0.6,
dashArray: '10',
});
};
const zoomTo = (e) => {
oMap.fitBounds(e.target.getBounds());
};
Leaflet.ChineseTmsProviders Provider for Chinese Tms Service
-
Leaflet 调用国内各种地图的功能十分复杂,幸好有 leaflet.ChineseTmsProviders 这个插件,这四种地图直接就可以加载进来,十分方便。
-
使用方法很简单可点击上面链接去 GitHub 看使用说明,或拉这个 demo下来来瞧一瞧代码。
-
提供了丰富多彩的图标 Leaflet.awesome-markers, See the demo map
-
强大的集聚插件 Leaflet.markercluster, See the demo map
-
优化的 label Leaflet.label, See the demo map
-
优化重叠在一起的 markers OverlappingMarkerSpiderfier-Leaflet, See the demo map
-
优化在边框上显示不在当前视野中的 marker Leaflet.EdgeMarker, See the demo map
- Leaflet-Develop-Guide 🍃 -开发文档及常用插件小结
- 引 leaflet 包的时候不要忘记引用包里的 css
import 'leaflet/dist/leaflet.css';
关于 Leaflet 和 esri-leaflet 一起使用 L.esri.TiledMapLayer 加载 ArcGIS 服务切片底图时,控制台打印报错 Uncaught ReferenceError: proj4 is not defined
- 查看了下源码
if (!proj4) { warn('L.esri.TiledMapLayer is using a non-mercator spatial reference. Support may be available through Proj4Leaflet http://esri.github.io/esri-leaflet/examples/non-mercator-projection.html');}
问题就出在这里,esri-leaflet 里的一个插件 proj4leaflet 依赖 proj4,所以需要手动引入 proj4 这个包。 - 这个 GitHub 上面的提问及回答 Github esri-leaflet Issues,原因是 leaflet 不支持该服务坐标系,需要依赖 proj4 进行坐标投影。
- 如果你是模块化开发,需要再
npm i proj4
然后再引入进来好了import * as proj4 from 'proj4'; window['proj4'] = proj4;
。 - 如果你是常规开发,直接添加一个 script 标签引用 CDN 资源上托管的 Proj4js 就是了
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.4.4/proj4-src.js"></script>
。
本文 DEMO 地址: https://github.com/lvisei/leaflet-demo