Node + Express + PostGIS 动态矢量切片

前言

基于 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>

结果

渲染结果如下:


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容