Mapbox系列 - 实现常用的四种的地图样式(区域、气泡、热力、迁徙)
-1、经历了两个月的地图开发总结了一些Mapbox的使用,也算是入门了,分享一下所得
0、正文中提到的资源
筛选语法 | 进阶mapbox GL之paint和filter
1、实现一个地图的基础框架
-
搭个空地图的架子出来
<template> <div ref="basicMapbox"></div> </template> <script> // 引入 mapbox 组件及汉化包 import mapboxgl from 'mapbox-gl'; import MapboxLanguage from '@mapbox/mapbox-gl-language'; import 'mapbox-gl/dist/mapbox-gl.css'; // mapbox 的必要配置 mapboxgl.accessToken = 'pk.eyJ1IjoiYmxhY2tzaWRldiIsImEiOiJjbGRmYjA2N2kwN3BoM29vMW45bWtqd3A2In0.dv_6gqoPysAqVj0I3H5tPA' // 可以从官网申请 也可以直接用我的 mapboxgl.setRTLTextPlugin("https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.1.0/mapbox-gl-rtl-text.js"); // 设置语言 export default { data() { return {} }, mounted() { this.initMap() }, methods: { initMap() { const map = new mapboxgl.Map({ container: this.$refs.basicMapbox, // 容器 style: "mapbox://styles/mapbox/streets-v11", // 地图的乱七八糟基础样式 这个就正常样式也可以自己配样式下面附链接 center: [118.726533,32.012005], // 初始坐标系:北京市(就这个就行了) zoom: 2, // 当前显示地图级别 minZoom: 2, // 最小显示地图级别 maxZoom: 15, // 最大显示地图级别 pitch: 0, //地图的角度,不写默认是0,取值是0-60度,一般在3D中使用 bearing: 0, //地图的初始方向,值是北的逆时针度数,默认是0,即是正北 antialias: true, //抗锯齿,通过false关闭提升性能,开着清晰 }) map.addControl(new MapboxLanguage({ defaultLanguage: 'zh-Hans' })); // 中文 map.addControl(this.navigation, 'top-left'); // 导航条 } } } </script>
-
载入你的
GeoJSON
资源使其可视化GeoJSON类型的数据专门用于加载地图,所以格式一定要正确,可以通过
geometry
的type
属性绘制点、线、面地图区域。{ 'type': 'FeatureCollection', 'features': [ { "type": "Feature", // 这里比较随意,可以自行定义,携带着geometry所指区域的信息,便于今后图例、值标签、颜色填充等相关工作进行 "properties": { "id": '12', "name": 'xxx', "cp": 'xx', }, // 地理相关,能够绘制出点线面的地图特征 "geometry": { "type": "Point", "coordinates": [116.3955,39.858682] } } ] }
有了这些数据之后就可以载入资源了
// 在第一步中的initMap中继续编码 let hoveredStateId = null; // 下面悬浮框时使用 map.on('load', () => { map.addSource("sourceName", { type: "geojson" /* geojson类型资源 */, data: ''// 你的geojson资源 }); // 到此为止 你的数据已经载入了, 现在需要将其以图层形式可视化出来 map.addLayer({ id: "china-fills", // 图层id source: "sourceName", // 图层的资源 type: "fill", // 例如我正在加载的是一个geometry为区域类型的资源, fill就会将区域地图特征绘制成多姿多彩的面 paint: { // 用 paint 设置这个面的各种样式 "fill-color": 'rgba(0, 0, 0, 1)', "fill-opacity": [ // 这是一种筛选语法 上面附上其他博主的链接可自行学习 'case', ['boolean', ['feature-state', 'hover'], false], // 鼠标滑入时的效果 0.8, 0.5 ] } }) })
-
数据可视化出来了 再加上悬浮框
// 紧接着第二步 // 区域地图 鼠标移动事件 map.on('mousemove', 'china-fills'/*图层id*/, (e) => { if (e.features.length > 0) { // 由于位置不断变化 先删掉存在的悬浮框 this.popup?.remove(); map.getCanvas().style.cursor = ''; map.setFeatureState( { source: 'china-boundary', id: hoveredStateId }, { hover: false } ); hoveredStateId = null; // 添加新悬浮框 map.getCanvas().style.cursor = 'pointer'; this.popup = this.createPopup(e, e.features[0].properties); this.popup.addTo(map); if (hoveredStateId !== null) { map.setFeatureState( { source: 'sourceName', id: hoveredStateId }, { hover: false } ); } hoveredStateId = e.features[0].id; map.setFeatureState( { source: 'sourceName', id: hoveredStateId }, { hover: true } ); } }); map.on('mouseleave', 'china-fills', () => { map.getCanvas().style.cursor = ''; if (hoveredStateId !== null) { map.setFeatureState( { source: 'sourceName', id: hoveredStateId }, { hover: false } ); } hoveredStateId = null; this.popup?.remove(); }) // 创建图例 function createPopup(e, feature) { this.popup?.remove(); // 取区域的颜色 const htmlText = [] const fields = feature.fields ? JSON.parse(feature.fields) : [] fields.map(field => { htmlText.push(`${field.name}:${field.value}<br>`) }) let html = ` <div class="popupTitle" style="background-color: transparent}" >${htmlText.join('')}</div>`; return new mapboxgl.Popup({ offset: 0, closeButton: false, className: '' }) .setLngLat([e.lngLat.lng, e.lngLat.lat]) .setHTML(html); },
2、使用频率较高 Mapbox API 介绍
getLayer
: 获取图层removeLayer
: 删除涂层setPaintProperty
: 设置、更新绘制类属性setLayoutProperty
: 设置、更新布局类属性loadImage/addImage
: 加载自定义图片removeControl
: 移除导航条addControl
: 添加导航条及其位置map.addControl(new mapboxgl.NavigationControl(), 'top-left')
resize
: 刷新
3、四类地图的实现 (实质上就是图层及GeoJson的应用)
***** 需要注意的是GeoJson
资源的格式要对应你的地图图层类型,比如你是点地图那就是point
类型,你是区域地图就是MultiPolygon
类型的GeoJson
-
区域地图
// 区域图层 map.addLayer( { id: "areaLayer", type: "fill" /* fill类型一般用来表示一个面,一般较大 */, source: "sourceName", paint: { "fill-color": 'rgba(0, 0, 0, 1)', "fill-opacity": [ 'case', ['boolean', ['feature-state', 'hover'], false], 0.8, 0.5 ] }, } ) // 你可以通过对fill-color的筛选定制各种区域色 这里不再赘述
-
气泡地图
map.addLayer({ 'id': 'bubbleLayer', 'source': 'sourceName', 'type': 'circle', 'paint': { 'circle-color': 'rgba(0, 0, 0, 1)', 'circle-radius': 6, 'circle-stroke-width': 2, 'circle-stroke-color': '#ffffff' } })
-
热力地图
map.addLayer({ 'id': 'heatmapLayer', 'type': 'heatmap', 'source': 'china-boundary', 'maxzoom': 9, 'paint': { // 根据频率和属性大小增加热图权重 'heatmap-weight': 1, // 按缩放级别增加热图颜色权重 热图强度是热图权重之上的乘数 'heatmap-intensity': [ 'interpolate', ['linear'], ['zoom'], 0, 1, 9, 3 ], // 热图的色带域为0(低)到1(高),从0档开始色带,颜色为0透明,0创建类似模糊的效果。 'heatmap-color': [ 'interpolate', ['linear'], ['heatmap-density'], 0, 'rgba(33,102,172,0)', 0.2, 'rgb(103,169,207)', 0.4, 'rgb(209,229,240)', 0.6, 'rgb(253,219,199)', 0.8, 'rgb(239,138,98)', 1, 'rgb(178,24,43)' ], // 通过缩放级别调整热图半径 'heatmap-radius': [ 'interpolate', ['linear'], ['zoom'], 0, 2, 9, 20 ], // 从热图到按缩放级别旋转的圆形图层 'heatmap-opacity': [ 'interpolate', ['linear'], ['zoom'], 7, 1, 9, 0 ] } }, 'waterway-label')
-
迁徙地图
迁徙地图需要引入
turf
包用于绘制迁徙曲线import * as turf from '@turf/turf' // npm i turf -S
迁徙地图的
GeoJson
及飞线动画构造<script> export default { data() { return { counter: 0, counterKey: 1, // 用于解决重复调用动画问题 steps: 500, // 步数越多意味着弧线和动画越流畅 pointGroups: [ { adcode: 1, name: '新疆维吾尔自治区', coordinates: [87.617733, 43.792818], data: '6320.1', }, { adcode: 2, name: '合肥市', coordinates: [117.283042, 31.86119], data: '3213.4', }, { adcode: 3, name: '北京市', coordinates: [116.405285, 39.904989], data: '5124.3', }, { adcode: 4, name: '拉萨市', coordinates: [91.132212, 29.660361], data: '4323.8', }, { adcode: 5, name: '长春市', coordinates: [125.3245, 43.886841], data: '8131.2', } ], linesMaps: [[1, 2], [1, 3], [4, 5], [1, 5]], // [[origin, destination]] 起终点映射关系 route: {'type': 'FeatureCollection', 'features': []}, // 绘制迁徙线 point: {'type': 'FeatureCollection', 'features': []}, // 绘制迁徙轨迹(绘制点动画) } }, created() { this.setMigrationMapData() }, methods: { /** * @description: 设置迁徙图基础数据 */ setMigrationMapData() { this.linesMaps.forEach(item => { const origin = this.pointGroups.find(e => e.adcode === item[0]).coordinates const destination = this.pointGroups.find(e => e.adcode === item[1]).coordinates this.route.features.push({ 'type': 'Feature', 'geometry': { 'type': 'LineString', 'coordinates': [origin, destination] } }) // 轨迹点 this.point.features.push({ 'type': 'Feature', 'properties': {}, 'geometry': { 'type': 'Point', 'coordinates': origin } }) }) // 计算路线起点/终点之间的距离(以公里为单位) this.route.features.map(feature => { const lineDistance = turf.length(feature) const arc = [] // 在两点的“起点”和“目的地”之间画一条弧线 for (let i = 0; i < lineDistance; i += lineDistance / this.steps) { const segment = turf.along(feature, i); arc.push(segment.geometry.coordinates); } // 使用计算出的弧坐标更新路径 feature.geometry.coordinates = arc; }) // 用于根据路径递增点测量值 this.counter = 0; }, /** * @description: 构造端点的 GEOJSON 数据 */ buildMigrationData() { let origins = []// 起点集合 let destinations = []// 终点集合 // 根据起终点映射分析出起终点集合 this.linesMaps.forEach(item => { if (!origins.find(e => e === item[0])) origins.push(item[0]) if (!destinations.find(e => e === item[1])) destinations.push(item[1]) }) const result = { origin: {'type': 'FeatureCollection', 'features': []}, destination: {'type': 'FeatureCollection', 'features': []} } const feature = (item) => { return { "type": "Feature", "properties": { "id": item.adcode, "name": item.name, "cp": item.coordinates, "showHover": true, "fields": [] }, "geometry": { "type": "Point", "coordinates": item.coordinates } } } origins.map(adcode => { const item = this.pointGroups.find(e => e.adcode === adcode) result.origin.features.push(feature(item)) }) destinations.map(adcode => { const item = this.pointGroups.find(e => e.adcode === adcode) result.destination.features.push(feature(item)) }) return result }, // 飞线动画 animate(counter, l_map, l_index) { if (!l_map) return const map = l_map const steps = this.steps const route = this.route const point = this.point const _this = this const counterKey = this.counterKey let timeHandle = null clearTimeout(timeHandle) animateFrame() function animateFrame() { // 当迁徙图标及动画设置处于开启状态时再请求下一帧以提高性能 if (!_this.migrationStyle.animate || !_this.migrationStyle.iconShow) { counter = 0 return } const start = route.features[l_index].geometry.coordinates[counter >= steps ? counter - 1 : counter]; const end = route.features[l_index].geometry.coordinates[counter >= steps ? counter : counter + 1 ]; if (!start || !end) return; // 根据计数器表示将点几何更新到新位置 point.features[l_index].geometry.coordinates = route.features[l_index].geometry.coordinates[counter]; // 计算方位角以确保图标旋转以匹配路线弧方位角 point.features[l_index].properties.bearing = turf.bearing(turf.point(start), turf.point(end)); // 使用此新数据更新源 map.getSource('point').setData(point); // 请求下一帧动画,只要尚未到达结束 counterKey === _this.counterKey 避免重复动画 if (counter < steps && `${counterKey}${l_index}` === `${_this.counterKey}${l_index}`) {requestAnimationFrame(animateFrame);} counter = counter + 1; if (counter === steps - 1) { const originByLIndex = _this.pointGroups.find(e => e.adcode === _this.linesMaps[l_index][0]) point.features[l_index].geometry.coordinates = originByLIndex.coordinates; map.getSource('point').setData(point); counter = 0; } } // 定时暂停动画 timeHandle = setTimeout(() => { counter = steps }, _this.migrationStyle.animate ? _this.migrationStyle.animateTime * 1000 * 60 : 0) }, } } </script>
迁徙图层相关代码
// import airplane from '@/assets/img/airplane.png' map.loadImage(airplane, (error, image) => { if(error) reject() map.addImage('airplane', image) // 小飞机图标 }) const migrationData = this.buildMigrationData() map.addSource("route", { lineMetrics: true, type: "geojson" /* geojson类型资源 */, data: this.route }); map.addSource("point", { type: "geojson" /* geojson类型资源 */, data: this.point }); map.addSource("originPoints", { type: "geojson" /* geojson类型资源 */, data: migrationData.origin }); map.addSource("destinationPoints", { type: "geojson" /* geojson类型资源 */, data: migrationData.destination }); // 起点气泡图层 map.addLayer({ 'id': 'originPoints', 'source': 'originPoints', 'type': 'circle', 'paint': { 'circle-color': '#4264fb', 'circle-radius': 6, 'circle-stroke-width': 2, 'circle-stroke-color': '#ffffff' } }) // 终点气泡图层 map.addLayer({ 'id': 'destinationPoints', 'source': 'destinationPoints', 'type': 'circle', 'paint': { 'circle-color': '#4264fb', 'circle-radius': 6, 'circle-stroke-width': 2, 'circle-stroke-color': '#ffffff' } }) // 迁徙线 map.addLayer({ 'id': 'route', 'source': 'route', 'type': 'line', 'paint': { 'line-width': 2, 'line-color': '#007cbf', "line-dasharray": [2, 2], // 虚线 实线为[2, 0] } }); // 迁徙动画 图标 map.addLayer({ 'id': 'point', 'source': 'point', 'type': 'symbol', 'layout': { // 此图标是地图框街道样式的一部分 'icon-image': 'airplane', 'icon-size': 1.25, 'icon-rotate': ['get', 'bearing'], 'icon-rotation-alignment': 'map', 'icon-allow-overlap': true, 'icon-ignore-placement': true } }); // 启动动画 for (let i = 0; i < this.route.features.length; i++) { this.animate(this.counter, map, i) }
5、结束!!
若有相关代码不理解 或有其他实现欢迎私信评论讨论…