使用mapboxgl 實現特定的地圖效果
最近完成的一個項目,dashboard 地圖模塊的需要和第三方對接,對接要求使用mapboxgl 來對接。以前的項目一直用leaflet庫來處理地圖需求,mapboxgl 庫對我來說很陌生。學習研究一段時間,在基本實現了產品設計的地圖交互功能后,我在這里寫記錄。
先上張設計效果圖:
一、要求實現的功能
1.加載深圳地圖瓦片、顏色采用暗色調。
2.地圖附有藍色遮罩層,鼠標hover時 ,該區域高亮並展示相應的數據。
3. 攝像頭點位在地圖上顯示,兩種類型,一個綠色一個藍色,要求有聚合功能並根據攝像頭類型和數量來決定icon在地圖上顯示綠色或者藍色。
二、代碼實現
mapboxgl底層是使用h5 的canvas 技術,這就決定了里面所有的鼠標事件都是根據鼠標的(x,y)坐標來觸發。同時也可以解析為什么地圖頁面被tranform拉伸(css3 樣式) 后, mapboxgl 的事件觸發會失靈的原因。
2.1.加載深圳地圖瓦片、顏色采用暗色調
2.1.1 加載深圳地圖瓦片(矢量地圖),地圖背景設置成圖紙規定的顏色
const mapStyle = 'xxxxx'; // 地圖的樣式配置,里面有一些地圖的基本信息和地圖一些圖層layers設置
var map = new mapboxgl.Map({
container: 'map', // container id
style: mapStyle ,
center: [114.185125079355, 22.6322002129776], // starting position
zoom: 10,
attributionControl: false
});
瓦片采用暗色調,如果mapStyle(文件具體格式參照mapboxgl官網:https://docs.mapbox.com/help/glossary/style/) 提供的地圖瓦片(矢量地圖瓦片)不是自己想要的色調,那就需要用代碼修改mapStyle里面的配置。
改地圖的一些屬性前,第一要解決的問題就是:我怎么知道地圖里面有哪些配置,哪些layers?
1.可以查看瀏覽器的network的接口抓包 。
2. 用console.log(map)打印地圖對象。
下面截圖來自mapboxgl 官網和supermap 官網例子:
地圖的所有信息都會寫入到map 對象里面,_layers里面記錄了所有覆蓋在地圖上面的圖層,里面是一個對象集合,每個對象里面都有layer 的id 、type、layout 、paint 等屬性。跟photoshop 一樣,canvas 繪制的地圖就是一層一層的圖層疊加起來的圖片,每一層圖層都有自己的一些設計,每個圖層都有自己指定的圖層序號(id)。要修改某個圖層只需要知道layer id 就行。
supermap地圖的第一層一般都是background 圖層,backgound 規定了地圖的背景顏色和透明度。上面的效果圖地圖的背景色是指定顏色的。mapboxgl 提供了 map.getLayers() 、 map.removeLayer()、map.addLayers() 、map.setPaintProperty()等api。supermap官網提供的地圖有background 這層layer,我這里只需要修改這一層圖層的顏色就行。
map.on('load',function(){
map.setPaintProperty('background', 'background-color', '#45516E');
})
2.1.2 修改地圖其他圖層的覆蓋物顏色
supermap 提供的地圖瓦片是白色瓦片,和UI設計不符。解決的方法有兩個,一要求supermap 直接提供深色主題的地圖瓦片,二前端自己處理,在地圖加載完成后切換瓦片的顏色。
layer 里面的 type 是說明當前圖層的類型。
type分為:
fill: 類似於canvas 里的fill,在給定的經緯度區域內填充內容
line: 沿着經緯度點畫線
symbol: 圖標或者label
circle: 在指定點位上畫圓形
heatmap:熱力圖
…
從type 上分析,地圖最顯眼圖層塊應該是type 為fill 和line 類型的layer。要切換地圖主題顏色,這就需要考慮這兩種類型。主要修改它們的fill-color 和line-color 屬性,同時還要考慮顏色層次和地形的區分,比如綠地、水系 、高速路、省道等不同覆蓋物使用不同的顏色或者不同的透明度。具體需要修改哪些layers,我們可以先研究下map 里面的layers ,再挑一些比較顯眼的瓦片layer 修改。
map.setPaintProperty('background', 'background-color', '#45516E');
// 獲取地圖上所有的layers,因為是遍歷object 對象,可以用object.keys來遍歷
Object.keys(map.style._layers).map(v=>{
const opt = map.style._layers[v];
// 修改綠地、水系的瓦片顏色
if((opt.id.includes('綠地')||opt.id.includes('水系')) && opt.type=='fill'){
map.setPaintProperty(opt.id, 'fill-color', '#182c4e');
}
// 修改道路的顏色
if((opt.id.includes('高速')||opt.id.includes('國道')||opt.id.includes('省道')) && opt.type=='line'){
map.setPaintProperty(opt.id, 'line-color', '#182c4f');
}
})
2.2 地圖藍色遮罩層,地區邊界線亮色顯示,在藍色遮罩層上添加區域名稱
2.2.1 地圖藍色遮罩層,地區邊界線亮色顯示
如果地圖有按照地區邊界區分的layer,可以考慮直接修改或者復制一個圖層疊在地圖最上端,這個方案是最簡單明了的。通常情況下和第三方對接,對方提供的東西很可能性不能完全滿足己方的需要。再對接后我獲取到的地圖layers 並沒有這樣一個圖層。這種情況該怎么處理?最優方案是反推對方,要求對方提供對應的文件。次選方案從網上搜索一個深圳相關的geoJson文件,然后加載到地圖上層。開發項目的時候,我同時執行了兩種方案。但最終只能使用此選方案。地圖體系不同,地圖邊界線的經緯度就有些偏差,網上下載的geoJson 加載到地圖上,放大后可以看出邊緣有一些部分不太重合,同時geoJson 圖層整體都有些偏移。這些都需要在地圖加載前做同樣的經緯度偏差處理。
var sourceName = 'blueMask'; // 資源名稱,自定義
map.addSource(sourceName, {
type: 'geojson',
data: geoJson, // 網上下載的geoJson的地圖文件,使用前經過偏差算法處理
});
// 藍色遮罩層顏色設定,透明度通過feature-state 的值的情況來設定顏色透明度
map.addLayer({
id: 'addlayermask',
type: 'fill',
source: sourceName,
layout: {
},
paint: {
'fill-color': '#286BFF',
'fill-opacity': ['case', ['boolean', ['feature-state', 'hover'], false], 0.3, 0],
},
});
// 設置地圖區域邊沿的線寬和顏色值
map.addLayer({
id: `${
sourceName}-line`,
type: 'line',
source: sourceName,
layout: {
},
paint: {
'line-width': 1.5,
'line-color': '#286BFF',
},
});
2.2.2 鼠標hover ,區域圖層高亮,並彈窗顯示區域的介紹信息:
let hoveredStateId = null;
let listener1 = function(e) {
map.getCanvas().style.cursor = 'pointer';// 設定鼠標移入的樣式
if (e.features.length > 0) {
if (hoveredStateId) {
map.setFeatureState({
source: sourceName, id: hoveredStateId }, {
hover: false });// 先還原成默認狀態
}
hoveredStateId = e.features[0].id; // ps:加載的geoJson feature 里面必須設定一個id 屬性,用於定位哪個區域需要高亮。如果原文件沒有,可以手動在原文件上添加id 屬性並設置對應的id 數字
map.setFeatureState({
source: sourceName, id: hoveredStateId }, {
hover: true });
// 鼠標hover 時 彈窗顯示區域的介紹信息
popup .setLngLat([lnglat[0], lnglat[1]]) // 彈窗的經緯度位置,可以設成下面區域名稱的經緯度附近坐標
.setHTML(
`<div class=cameraDes>
區域信息介紹
</div>`,
)
.addTo(map);
}
};
map.on('mousemove', 'addlayermask', listener1);
// 鼠標移出事件,改變hover 的值
let listener2 = function() {
map.getCanvas().style.cursor = ''; //改變鼠標樣式
if (hoveredStateId) {
map.setFeatureState({
source: sourceName, id: hoveredStateId }, {
hover: false });
}
hoveredStateId = null;// 還原或者情況
};
// 鼠標離開時 去掉高亮狀態
map.on('mouseleave', 'addlayermask', listener2);
})
2.2.3 在藍色遮罩層上添加區域名稱
/**
* 增加區域的名稱和區域名字
* @param map 地圖實例
* @param markClass marker 的頁面樣式
* @param geoJson geoJson 格式的地圖數據
*/
export function addRegionName(map, markClass, geoJson) {
geoJson.features.forEach((v) => {
const el = document.createElement('div');
el.className = markClass;
const t = document.createTextNode(v.properties.name);
el.appendChild(t);
new mapboxgl.Marker({
element: el,// 只支持原生的html 元素
})
.setLngLat(v.properties.center)// 使用geoJson 里面的center 屬性來
.addTo(map);
});
}
2.3 攝像頭點位在地圖上顯示,兩種類型,一個綠色一個藍色,要求有聚合功能並根據攝像頭類型和數量來決定顯示綠色或者藍色
如果單單只是要實現攝像頭點位的藍綠色圖標,mapboxgl提供了marker 、circle 、canvas 、symbol。這里我直接采用最簡單的circle ,同時也方便后面的cluster 處理。
const sourceName = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point', // 攝像頭使用point類型,在地圖上渲染
coordinates: [0, 0],// 攝像頭的經緯度
},
properties: {
title: 'camera',
areaType: 1,// 自定義屬性,
},
},
],
};
map.addLayer({
id: layerId,// 這個id 是自定義的,layerId 是通過函數的參數傳遞進來的
type: 'circle',
filter: ['!', ['has', 'point_count']], // 渲染條件,只渲染沒有point_count 屬性的點位。point_count 屬性是聚合cluster 的屬性。這里只渲染非聚合的
source: sourceName,// sourceName,格式為geoJson,areaType 是自定義的屬性,可以在渲染前把所有的攝像頭點位寫入sourceName 變量里面
paint: {
'circle-color': ['case', ['==', ['get', 'areaType'], 1], '#286bff', '#0ebd73'],// 通過areaType 類型判斷攝像頭應該渲染什么顏色。如果 areaType === 1 渲染#286bff,不等就渲染#0ebd73
'circle-radius': 5, //攝像頭圓圈的半徑,5px
},
});
// Create a popup, 鼠標hover時攝像頭彈窗顯示攝像頭名稱.
const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,
});
map.on('mouseenter', layerId, function(e) {
isHoverCameraIcon = true;
// districtPop 是全局變量,這里做了彈窗的一個復位操作
districtPop !== null && districtPop.remove();
map.getCanvas().style.cursor = 'pointer';
const coordinates = e.features[0].geometry.coordinates.slice();
const description = e.features[0].properties.title; // 攝像頭名稱,屬性是自定義的
// Ensure that if the map is zoomed out such that multiple
// copies of the feature are visible, the popup appears
// over the copy being pointed to.
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
// Populate the popup and set its coordinates
// based on the feature found.
if (description) {
popup
.setLngLat(coordinates)
.setHTML(`<div class=cameraDes>${
description}</div>`)// 彈窗的具體內容
.addTo(map);
}
});
map.on('mouseleave', layerId, function() {
isHoverCameraIcon = false;
map.getCanvas().style.cursor = '';
popup.remove();
});
2.3.1 攝像頭聚合功能,顏色定義,聚合圖標顯示攝像頭數量等功能
const mag1 = ['==', ['get', 'areaType'], 1];
const mag2 = ['==', ['get', 'areaType'], 2];
//添加聚會的圖層的source
map.addSource( 'marker_market', {
type: 'geojson',
data: sourceName ,// 取上面的geojson 格式的文件
cluster: true,
clusterMaxZoom: 12,//允許聚合圖層最大的放大圖層
clusterRadius: 20,//聚合后的攝像頭圖標半徑
clusterProperties: {
mag1: ['+', ['case', mag1, 1, 0]], // 統計聚合點areaType ==1 的數量,累計如果滿足mag1 的條件 ,clusterProperties 的mag1 的值就加1 否則加0
mag2: ['+', ['case', mag2, 1, 0]], // 統計聚合點areaType ==2 的數量
},
});
// 添加cluster 的
map.addLayer({
id: layerId, // layerId 隨便一個字符都行,不和其他layer 重名就好
type: 'circle',
filter: ['has', 'point_count'],// 只處理擁有point_count 屬性的的攝像頭點位
source: sourceName,
paint: {
'circle-color': ['case', ['>=', ['get', 'mag1'], ['get', 'mag2']], '#286bff', '#0ebd73'], // 如果cluster 的mag1 屬性大於 mag2 屬性,優先顯示#286bff攝像頭顏色
'circle-radius': ['step', ['get', 'point_count'], 5, 1, 10, 10, 12],// 聚合攝像頭數量 1個 圓的半徑為5px,1~10 個攝像頭 圓的半徑為10px,10個攝像頭以上,半徑為12px
},
});
// 在聚合cluster圖標中間渲染攝像頭的數量
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: sourceName,
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12,// 規定字體的字號
},
paint: {
'text-color': 'rgba(255,255,255,1)', // 字體顏色
},
});
// 匯聚點擊即后,自動展開匯聚點。官網有這個例子
map.on('click', layerId, function(e: any) {
const features = map.queryRenderedFeatures(e.point, {
layers: [layerId],
});
const clusterId = features[0].properties.cluster_id; // features
if (clusterId) {
map.getSource(sourceName).getClusterExpansionZoom(clusterId, function(err: any, zoom: any) {
if (err) return;
map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom,
});
});
}
});
如果攝像頭有變動,需要修改layer 的信息,這時有兩種方法。
1.刪除原圖層然后添加新圖層 :map.getLayer(layer) && map.removeLayer(layer)
2. 直接用mapboxgl 提供的api修改layer的屬性: map.getLayer(‘background’) && map.setPaintProperty(‘background’, ‘background-color’, ‘rgba(4,21,37,1)’);
這樣整地圖除底層的地圖瓦片外,其他的效果差不多就已經實現了。