zoukankan      html  css  js  c++  java
  • 行政区划遮罩 -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)’);

    这样整地图除底层的地图瓦片外,其他的效果差不多就已经实现了。

  • 相关阅读:
    使用shell生成excel
    linux逻辑卷扩容
    nginx异常访问处理
    云主机上的k8s集群通信故障
    【转】 Pro Android学习笔记(七九):服务(4):远程服务的实现
    老李分享:系统可用性评估
    Develoment 和 Production 模式的区分打包
    TS基础学习笔记
    flanneld 安装
    Execl文件批量转csv文件
  • 原文地址:https://www.cnblogs.com/hustshu/p/14902276.html
Copyright © 2011-2022 走看看