使用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)’);
这样整地图除底层的地图瓦片外,其他的效果差不多就已经实现了。