寫在前面
WebGIS 開發基礎之 Leaflet
1. GIS Web開發基本概念:
GIS、Map、Layer、Feature、Geometry、Symbol、Data(Point、Polyline、Polygon)、Renderer、Scale、Project、Coordinates;
2. GIS Web 開發概述:
架構模式、常用平臺和 SDK、二維、三維
3. 使用 Leaflet 開發常用功能:
- 地圖加載(底圖類型、切換)
- 地圖操作(縮放、平移、定位/書簽、動畫)
- 圖層管理(加載、移除、調整順序)
- 要素標繪(點/聚簇、線、面,符號化/靜態動態)
- 屬性標注(字段可選、樣式定制)
- 專題地圖(點、線、面,渲染)
- 查詢定位(屬性查詢、空間查詢/周邊搜索/緩沖區/面查點線面/點線查面、圖屬互查、綜合查詢)
- 信息窗口(入口、Popup、定制)
- 坐標轉換(地理與投影、不同地理坐標系)
- 空間運算(長度面積測量、點取坐標、緩沖區、相交包含關系)
- 動態監控(固定點狀態切換、車輛監控)
4. Leaflet 常用 API
<a >]</a>
Demo 用到的庫
- 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, //要使用的坐標參考系統,默認的坐標參考系,互聯網地圖主流坐標系
// crs: L.CRS.EPSG4326, //WGS 84坐標系,GPS默認坐標系
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:
'liuvigongzuoshi@foxmail.com © <a ,
}).addTo(map);
console.log(Baselayer);
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);
console.log(Baselayer);
}
};
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('/* 地理位置服務不可用 */');
mapInit([30.626866, 104.152894]); //指定一個數據 定位在成都北緯N30°37′45.58″ 東經E104°09′1.44″
}
- 定位成功或失敗處理方法
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('/* 地理位置請求失敗 */');
mapInit([31.626866, 104.152894]); //指定一個數據 定位在成都北緯N30°37′45.58″ 東經E104°09′1.44″
};
- 成功后初始化底圖
const mapInit = (LatLng) => {
map = L.map('mapDiv', {
crs: L.CRS.EPSG3857, //要使用的坐標參考系統,默認的坐標參考系,互聯網地圖主流坐標系
// crs: L.CRS.EPSG4326, //WGS 84坐標系,GPS默認坐標系
zoomControl: true,
// minZoom: 1,
attributionControl: true,
}).setView(LatLng, 18); //定位在當前位置
Baselayer = L.tileLayer(urlTemplate.mapbox_Image, {
maxZoom: 17, //最大視圖
minZoom: 2, //最小視圖
attribution:
'liuvigongzuoshi@foxmail.com © <a ,
}).addTo(map);
L.marker(LatLng, {
highlight: 'permanent', //永久高亮顯示
}).addTo(map);
console.log(Baselayer);
};
- 更多內容
- 更多了解 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 = () => {
map.panTo([37.91082, 128.73583], {
animate: true,
}); //地圖平移,默認就是true,將地圖平移到給定的中心。如果新的中心點在屏幕內與現有的中心點不同則產生平移動作。
};
- 地圖飛到中心點
const flyTo = () => {
map.flyTo([36.52, 120.31]); // 點到點的拋物線動畫,平移加縮放動畫
};
注意:盡量避免 setZoom()等地圖縮放方法與 flyTo、flyToBounds 一起合用,因為這兩類地圖操作方法都有各自的縮放值,造成動畫不流暢、不能定位到目的點。
- 地圖飛到邊界的合適的位置
const flyToBounds = () => {
map.flyToBounds(polygon.getBounds()); //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 = () => {
console.log(polygon.getBounds());
map.fitBounds(polygon.getBounds()); //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,
useCors: false, //是否瀏覽器在跨域的情況下使用GET請求。
}).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,
useCors: false, //是否瀏覽器在跨域的情況下使用GET請求。
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',
iconSize: [300, 79], // size of the icon
popupAnchor: [0, -10] // point from which the popup should open relative to the iconAnchor
});
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下來來瞧一瞧代碼。
優化 marker 相關的插件
提供了豐富多彩的圖標 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 學習資料整理
- 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/liuvigongzuoshi/leaflet-demo