前端地图绘制,基于Maplibre的管线流动效果

492 阅读3分钟

基于Maplibre的管线流动效果

由于最近开始了新的项目,技术上要求将模型中的管道节点使用二维地图渲染,管线带有流动动画效果, 并对地图性能,动画效果有比较高的要求,因此选择了maplibre作为基础地图框架,并使用antv L7图层绘制进行开发。
Maplibre是一款在Mapbox开源代码上进行了二次封装开发的框架,属于完全开源。

image.png

Maplibre国内使用较少,相关文档较少,官网案例中并未提供动态管线流动的案例,并且使用webgl绘制管线流动动画对于纯web前端来说学习成本太高,因此考虑使用了超图团队开发的Supermap for maplibre框架,感兴趣的小伙伴可以去官网查看具体案例 iclient.supermap.io/examples/ma… 话不多说,下面开始介绍具体使用方式,首先是引入Supermap for maplibre框架,官网提供了两种方式。
第一种:直接npm
npm install @supermapgis/iclient-maplibregl
第二种:通过js包引入,iclient.supermap.io/web/downloa…在官网地址中下载maplibregl对应的js包

image.png

项目中使用了第二种引入方式,为的是将来方便项目私有化部署。
一、script标签引人,include属性表示maplibregl中需要包含的其他技术,如antv的L7。

<script type="text/javascript" include="L7" src="../src/dist/maplibregl/include-maplibregl.js"></script>

二、引入maplibregl包之后开始进行地图开发,首先初始化地图实例(更多地图配置可以直接参考maplibre官方文档maplibre.org/maplibre-gl…

// 初始化
let map = new maplibregl.Map({
  container: 'map', // container id
  style: '../src/assets/VectorWithLabel_en.json', //地图矢量切片
  center: [114.177, 22.4819], // starting position
  pitchWithRotate: false, // 旋转时倾斜角改变
  zoom: 12,
  attributionControl: true
});

三、接着在地图加载之后开始绘制管道图层,使用supermap封装的方法新建L7图层实例,图层配置的type为L7官网各类型图层名称。

let layer = new maplibregl.supermap.L7Layer({ type: 'LineLayer' }); // 管线流动图层
let layer2 = new maplibregl.supermap.L7Layer({ type: 'LineLayer', options: {enablePropagation: true, pickingBuffer: 6} }); // 基础管道图层

详情可参考L7官网文档l7.antv.antgroup.com/api/line_la…

四、配置管道图层数据,样式。

通过source来配置管线图层的数据,数据类型可以为JSON,Geojson,csv等,详情可参考l7.antv.antgroup.com/api/line_la…

let l7Layer2 = layer2.getL7Layer();
let l7Layer = layer.getL7Layer();
let lineData = []
let pipeDataReverse = JSON.parse(JSON.stringify(pipeData)) 
// pipeData 数据格式为[[[x1, y1],[x2,y2]]] 三维数组
pipeDataReverse.forEach((item, index) => {
  item.forEach(its =>{
    its.reverse()
  })
  lineData.push({color:[
    255,
    255,
    102
  ], path: item, id:index, name: '管道' + index })
})
l7Layer
  .source(lineData, {
    parser: {
      type: 'json',
      coordinates: 'path'
    }
  })

通过size(控制管线直径),shape(管线样式,如曲线、直线等),color(管线颜色),select(选中管线颜色)控制绘制管线的具体展示。管道的动画效果则是通过animate来配置,配置参数如下:

  • duration 动画时间 单位(s)秒
  • interval 轨迹间隔, 取值区间 0 - 1
  • trailLength 轨迹长度 取值区间 0 - 1

其他参数的配置的具体参数可以参考官方文档l7.antv.antgroup.com/api/line_la…

l7Layer
  .source(lineData, {
    parser: {
      type: 'json',
      coordinates: 'path'
    }
  })
  .size(5)
  .shape('line')
  .color('color', (v) => {
    return `rgba(18,255,255, 0.8)`;
  })
  .animate({
    interval: 0.7,
    trailLength: 0.5,
    duration: 2
  });

五、管线图层绑定maplibre,图层事件监听等

完成图层的配置工作以后需要将管线图层与地图进行绑定.

map.addLayer(layer2);

l7Layer2.on('click', (e) => {
  const popup = new L7.Popup({
    offsets: [0, 0],
    closeButton: true
  })
    .setLnglat(e.lngLat)
    .setHTML(`<span> 管道: ${e.feature.name}</span>`);
  scene.addPopup(popup);
});

以上部分完成之后就可以得到一个流畅运行的地图管线动画效果了。以下是完整代码

地图绘制管道完整代码

<html>
<head>
    <meta charset='utf-8'/>
    <title>管道流量专题图</title>
    <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no'/>
    <script type="text/javascript" include="L7" src="../src/dist/maplibregl/include-maplibregl.js"></script>
    <script type="text/javascript" src="../src/components/pipeData.js"></script>
    <script type="text/javascript" src="../src/components/flowData.js"></script>
    <style>
        body {
            margin: 0;
            padding: 0;
        }

        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }
    </style>
</head>
<body>
<div id='map'></div>
<div style="display: flex;
    position: fixed;
    bottom: 20px;
    left: 20px;
    background-color: white;
    padding: 4px 10px;
    align-items: center;
    gap: 10px;
    font-size: 14px;"> 
    <div>流量(m³/s)</div>
    <div style="display: flex;">
        <div style="background-color: rgb(0, 62, 170); width: 24px; height: 18px;"></div>
        <div style="background-color: rgb(2,167,240); width: 24px; height: 18px;"></div>
        <div style="background-color: rgb(129,211,248); width: 24px; height: 18px;"></div>
        <div style="background-color: rgb(225,225,225); width: 24px; height: 18px;"></div>
    </div>
</div>
<script type="text/javascript">
   var tileURL = 'https://iclient.supermap.io/iserver/services/map-china400/rest/maps/ChinaDark/zxyTileImage.png?z={z}&x={x}&y={y}';
    var map = new maplibregl.Map({
        container: 'map', // container id
        style: {
          version: 8,
          sources: {
            'raster-tiles': {
              type: 'raster',
              tiles: [tileURL],
              tileSize: 256
            }
          },
          layers: [
            {
              id: 'simple-tiles',
              type: 'raster',
              source: 'raster-tiles',
              minzoom: 0,
              maxzoom: 22
            }
          ]
        },
        center: [114.177, 22.4819], // starting position
        zoom: 12,
        attributionControl: true
    });
    map.addControl(new maplibregl.NavigationControl(), 'top-left');
    // map.on('load', function () {
    //     debugger
    //         map.addStyle(geoData)
    //     })
    function divideArrayIntoQuartiles(arr) {
        let absData =  arr.map(item => Math.abs(item))
        // 第一步:对数组进行排序
        const sortedArray = absData.slice().sort((a, b) => a - b);
        
        // 第二步:计算四分位数
        function getQuartile(sortedArr, quartile) {
            const index = (sortedArr.length - 1) * quartile;
            const lowerIndex = Math.floor(index);
            const upperIndex = Math.ceil(index);
            if (lowerIndex === upperIndex) {
                return sortedArr[lowerIndex];
            }
            return sortedArr[lowerIndex] + (sortedArr[upperIndex] - sortedArr[lowerIndex]) * (index - lowerIndex);
        }

        const Q1 = getQuartile(sortedArray, 0.75);
        const Q2 = getQuartile(sortedArray, 0.8); // 中位数
        const Q3 = getQuartile(sortedArray, 0.9);
        console.log(Q1, Q2, Q3, sortedArray);
        // 返回结果
        return { Q1, Q2, Q3 };
    }

// 示例使用

const quartiles = divideArrayIntoQuartiles(flowData);

    map.on('load', function () {
        map.getL7Scene().then((scene) => {
            
            var layer = new maplibregl.supermap.L7Layer({ type: 'LineLayer', options:{
              minZoom: 15
            } });
            var layer2 = new maplibregl.supermap.L7Layer({ type: 'LineLayer', options: {enablePropagation: true, pickingBuffer: 6} });
            var l7Layer2 = layer2.getL7Layer();
            var l7Layer = layer.getL7Layer();
            let lineData = []
            pipeData.forEach((item, index) => {
                item.forEach(its =>{
                    its.reverse()
                })
                if(flowData[index] < 0){
                    item.reverse()
                }
                lineData.push({color:[
                    255,
                    255,
                    102
                ], path: item, id:index, name: '管道' + index, flow: Math.abs(flowData[index]) })
            })
            l7Layer
              .source(lineData, {
                parser: {
                  type: 'json',
                  coordinates: 'path'
                }
              })
              .size(5)
              .shape('line')
              .color('color', (v) => {
                return `rgba(18,255,255, 0.9)`;
              })
              .animate({
                interval: 0.7,
                trailLength: 0.5,
                duration: 2
              });
            l7Layer2
              .source(lineData, {
                parser: {
                  type: 'json',
                  coordinates: 'path'
                }
              })
              .size(2)
              .select({
                color: 'red',
                mix: 0,
                })
              .shape('line').color('flow', (value) => {
                if(value<quartiles.Q1){
                    return 'rgb(225,225,225)';
                }else if(value>=quartiles.Q1 && value<quartiles.Q2){
                  return 'rgb(129,211,248)';
                    
                }else if(value>=quartiles.Q2 && value<quartiles.Q3){
                    
                    return 'rgb(2,167,240)';
                }else{
                  
                    return 'rgb(0, 62, 170)';
                }
                });
            map.addLayer(layer);
            map.addLayer(layer2);
            
            l7Layer2.on('click', (e) => {
                console.log(e);
                const popup = new L7.Popup({
                  offsets: [0, 0],
                  closeButton: true
                })
                  .setLnglat(e.lngLat)
                  .setHTML(`<div> 管道: ${e.feature.name}</div>
                            <div> 流量: ${e.feature.flow}</div>
                  `);
                scene.addPopup(popup);
               });
        })
      });
</script>
</body>
</html>