行政區划遮罩 -mapbox


使用mapboxgl 實現特定的地圖效果

最近完成的一個項目,dashboard 地圖模塊的需要和第三方對接,對接要求使用mapboxgl 來對接。以前的項目一直用leaflet庫來處理地圖需求,mapboxgl 庫對我來說很陌生。學習研究一段時間,在基本實現了產品設計的地圖交互功能后,我在這里寫記錄。
先上張設計效果圖:
pic_5e61874e.png

一、要求實現的功能

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 官網例子:
pic_1359606e.png
pic_4148929b.png
地圖的所有信息都會寫入到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)’);

這樣整地圖除底層的地圖瓦片外,其他的效果差不多就已經實現了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM