Mapbox系列 - 实现常用的四种的地图样式(区域、气泡、热力、迁徙)

Mapbox系列 - 实现常用的四种的地图样式(区域、气泡、热力、迁徙)

-1、经历了两个月的地图开发总结了一些Mapbox的使用,也算是入门了,分享一下所得

请添加图片描述

0、正文中提到的资源

筛选语法 | 进阶mapbox GL之paint和filter

地理代码查询 | datav

香芋芋圆 | Mapbox常用配置项

Mapbox | 官网示例

1、实现一个地图的基础框架
  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>
    
  2. 载入你的GeoJSON资源使其可视化

    GeoJSON类型的数据专门用于加载地图,所以格式一定要正确,可以通过geometrytype属性绘制点、线、面地图区域。

    {
      '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
          ]
        }
      })
    })
    
  3. 数据可视化出来了 再加上悬浮框

    // 紧接着第二步
    // 区域地图 鼠标移动事件
    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、结束!!

​ 若有相关代码不理解 或有其他实现欢迎私信评论讨论…

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
引用\[3\]中的代码展示了如何使用react-mapbox-gl实现地图搜索。在代码中,我们首先导入了ReactMapboxGl和其他必要的组件。然后,我们创建了一个Map组件,并传入了一个accessToken作为参数。接下来,我们定义了一个newStyle对象,其中包含了地图样式和图层信息。在这个例子中,我们使用了天地图的瓦片服务作为地图的底图。然后,我们在Map组件中使用了这个newStyle作为地图样式,并设置了地图的容器大小和中心点坐标。最后,我们在地图上添加了一个标记点,并设置了标记点的图标样式。通过这样的方式,我们可以在地图实现地图搜索功能。 #### 引用[.reference_title] - *1* *2* [react-native-mapbox-gl在RN中的使用 --工作笔记](https://blog.csdn.net/simper_boy/article/details/105654598)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [React-mapbox-gl](https://blog.csdn.net/qq_34870529/article/details/103823205)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ChristmasFox&

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值