美文网首页开源
Node + Express + PostGIS 动态矢量切片

Node + Express + PostGIS 动态矢量切片

作者: 芒果香蕉_ | 来源:发表于2021-02-24 16:56 被阅读0次

    前言

    基于 PostGIS 实现空间数据动态矢量切片,提升大规模空间数据的前端渲染流畅度。主要思路为:

    • 根据前端请求的切片等级和行列号,计算切片边界范围;
    • 根据切片边界范围拼写 SQL 语句,生成相应的矢量切片。

    切片边界范围计算

    各种地图 API 通常是根据缩放等级、地图中心点、屏幕坐标等信息计算出该屏幕范围内所有地图瓦片的行列号,以及各个瓦片在屏幕中的位置,然后根据缩放等级(z)、瓦片列号(x)、 瓦片行号(y)向后台请求对应的地图瓦片进行展示。以 Mapbox gl 为例,请求路径为:

    http://127.0.0.1:3000/getMvt/{z}/{x}/{y}
    

    对后台(地图服务器)而言,需要根据 z、x、y 这三个值找到相应的地图瓦片返回给前端。既然是动态矢量切片,则后台需要根据接收到的 z、x、y 这三个值计算对应切片的边界范围,然后使用 PostGIS 计算出该范围的矢量瓦片返回给前端。如何根据缩放等级和瓦片编号计算瓦片左上角坐标,进而计算出瓦片经纬度范围,参见这篇文章

    代码逻辑

    首先设置路由:

    router.get('/getMvt/*/*/*', (req, res, next) => {
      spatial.getMvt(req, res, next);
    });
    

    然后通过解析请求路径,获取相应的 x, y, z 的值,拼写 SQL 语句,计算矢量切片:

    const pgConfig = require('./pgConfig');
    const pg = require('pg');
    const pool = new pg.Pool(pgConfig);
    
    let spatial = {
      // 生成矢量瓦片
      getMvt: function (req, res, next) {
        let temp = req.url
        let txyz = {
          x: parseInt(req.url.split('/')[3]),
          y: parseInt(req.url.split('/')[4]),
          z: parseInt(req.url.split('/')[2]),
        }
        let [xmin, ymin] = xyz2lonlat(txyz.x, txyz.y, txyz.z)
        let [xmax, ymax] = xyz2lonlat(txyz.x + 1, txyz.y + 1, txyz.z)
    
        let sql1 =
          ` 
          SELECT  ST_AsMVT(P,'point',4096,'geom') AS "mvt"
          FROM
          (
              SELECT  ST_AsMVTGeom(ST_Transform(geom,3857),ST_Transform(ST_MakeEnvelope (${xmin},${ymin},${xmax},${ymax},4326),3857),4096,64,TRUE) geom
              FROM "osm_pois_pt" 
          ) AS P
          `
    
        let sql2 =
          ` 
          SELECT  ST_AsMVT ( P,'line',4096,'geom' ) AS "mvt"
          FROM
          (
              SELECT  ST_AsMVTGeom (ST_Transform (geom, 3857 ),ST_Transform (ST_MakeEnvelope ( ${xmin},${ymin},${xmax},${ymax},4326 ),3857),4096,64,TRUE ) geom
              FROM "osm_roads_ln" 
          ) AS P 
          `
    
        let sql3 =
          ` 
          SELECT  ST_AsMVT ( P,'polygon',4096,'geom' ) AS "mvt"
          FROM
          (
              SELECT  ST_AsMVTGeom (ST_Transform (ST_Simplify(geom, 0.0),3857 ),ST_Transform (ST_MakeEnvelope ( ${xmin},${ymin},${xmax},${ymax},4326 ),3857),4096,64,TRUE ) geom
              FROM "osm_landuse_pn" 
          ) AS P
          `
    
        let SQL = `select (${sql1})||(${sql2})||(${sql3}) as mvt`;
        pool.connect((isErr, client, done) => {
          client.query(
            SQL,
            function (isErr, result) {
              done();
              if (isErr) {
                res.json(isErr);
              } else {
                // res.send(result.rows[0].mvt);
                res.send(result.rows[0].mvt);
              }
            }
          );
        })
      }
    };
    
    // 瓦片编号转经纬度
    function xyz2lonlat (x, y, z) {
      const n = Math.pow(2, z);
      const lon_deg = (x / n) * 360.0 - 180.0;
      const lat_rad = Math.atan(Math.sinh(Math.PI * (1 - (2 * y) / n)));
      const lat_deg = (180 * lat_rad) / Math.PI;
      return [lon_deg, lat_deg];
    }
    
    module.exports = spatial
    

    矢量瓦片的生成主要用到了 ST_AsMVT 和 ST_AsMVTGeom 这两个函数,通过函数 xyz2lonlat 得到相应边界顶点坐标。代码中三个SQL语句分别展示了点、线、面的矢量切片生成方法。可以看到,可以通过多个 SQL 语句分别对多个图层进行切片,最后合成一个总的 SQL,实现多图层的统一切片。

    前端代码如下:

    <!DOCTYPE html>
    <html>
    
    <head>
      <meta charset="utf-8" />
      <title>Add a vector tile source</title>
      <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
      <script src="https://api.mapbox.com/mapbox-gl-js/v2.0.1/mapbox-gl.js"></script>
      <link href="https://api.mapbox.com/mapbox-gl-js/v2.0.1/mapbox-gl.css" rel="stylesheet" />
      <style>
        body {
          margin: 0;
          padding: 0;
        }
        #map {
          position: absolute;
          top: 0;
          bottom: 0;
          width: 100%;
        }
      </style>
    </head>
    
    <body>
      <div id="map"></div>
      <script>
        mapboxgl.accessToken = 'xxx';
        let mapStyle = {
          version: 8,
          name: "Dark",
          sources: {
            mapbox: {
              type: "vector",
              url: "mapbox://mapbox.mapbox-streets-v8"
            }
          },
          sprite: "mapbox://sprites/mapbox/dark-v10",
          glyphs: "mapbox://fonts/mapbox/{fontstack}/{range}.pbf",
          layers: []
        };
        var map = new mapboxgl.Map({
          container: 'map',
          // style: 'mapbox://styles/mapbox/light-v10',
          style: mapStyle,
          zoom: 11,
          center: [114.0, 22.6]
        });
    
        map.on('load', function () {
          map.addSource('test_postgis', {
            type: 'vector',
            scheme: "xyz",
            tiles: ['http://127.0.0.1:3000/getMvt/{z}/{x}/{y}']
          });
          map.addLayer({
            'id': 'test_polygon',
            'type': 'fill',
            'source': 'test_postgis',
            'source-layer': 'polygon',
            "paint": {
              "fill-color": "rgba(0,222,0,0.8)",
              "fill-outline-color": "rgba(179,212,245,1)"
            }
    
          });
          map.addLayer({
            'id': 'test_polyline',
            'type': 'line',
            'source': 'test_postgis',
            'source-layer': 'line',
            'layout': {
              'line-join': 'round',
              'line-cap': 'round'
            },
            'paint': {
              'line-color': '#ff0000',
              'line-width': 1
            }
          });
          map.addLayer({
            'id': 'test_point',
            'type': 'circle',
            'source': 'test_postgis',
            'source-layer': 'point',
            'paint': {
              'circle-radius': 5,
              'circle-color': '#0000ff'
            }
    
          });
        });
      </script>
    </body>
    </html>
    

    结果

    渲染结果如下:


    最后,本文未考虑海量数据的性能优化,当缩放等级较小时,请求数据量变大,必然会影像性能,这时可对不同缩放等级的请求做不同处理,例如数据抽稀根据不同等级显示不同属性的数据等。
    源码地址

    相关文章

      网友评论

        本文标题:Node + Express + PostGIS 动态矢量切片

        本文链接:https://www.haomeiwen.com/subject/bwpsfltx.html