用MapBoxGL实现设计图效果

214次阅读
没有评论

先上张设计效果图:
用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 官网例子:
用MapBoxGL实现设计图效果
用MapBoxGL实现设计图效果 地图的所有信息都会写入到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)’);

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