前言
基于 PostGIS 實(shí)現(xiàn)空間數(shù)據(jù)動(dòng)態(tài)矢量切片泉褐,提升大規(guī)母蛱唬空間數(shù)據(jù)的前端渲染流暢度。主要思路為:
- 根據(jù)前端請(qǐng)求的切片等級(jí)和行列號(hào)塌衰,計(jì)算切片邊界范圍诉稍;
- 根據(jù)切片邊界范圍拼寫(xiě) SQL 語(yǔ)句蝠嘉,生成相應(yīng)的矢量切片。
切片邊界范圍計(jì)算
各種地圖 API 通常是根據(jù)縮放等級(jí)杯巨、地圖中心點(diǎn)蚤告、屏幕坐標(biāo)等信息計(jì)算出該屏幕范圍內(nèi)所有地圖瓦片的行列號(hào),以及各個(gè)瓦片在屏幕中的位置服爷,然后根據(jù)縮放等級(jí)(z)杜恰、瓦片列號(hào)(x)、 瓦片行號(hào)(y)向后臺(tái)請(qǐng)求對(duì)應(yīng)的地圖瓦片進(jìn)行展示仍源。以 Mapbox gl 為例心褐,請(qǐng)求路徑為:
http://127.0.0.1:3000/getMvt/{z}/{x}/{y}
對(duì)后臺(tái)(地圖服務(wù)器)而言,需要根據(jù) z笼踩、x逗爹、y 這三個(gè)值找到相應(yīng)的地圖瓦片返回給前端。既然是動(dòng)態(tài)矢量切片嚎于,則后臺(tái)需要根據(jù)接收到的 z掘而、x、y 這三個(gè)值計(jì)算對(duì)應(yīng)切片的邊界范圍于购,然后使用 PostGIS 計(jì)算出該范圍的矢量瓦片返回給前端袍睡。如何根據(jù)縮放等級(jí)和瓦片編號(hào)計(jì)算瓦片左上角坐標(biāo),進(jìn)而計(jì)算出瓦片經(jīng)緯度范圍肋僧,參見(jiàn)這篇文章斑胜。
代碼邏輯
首先設(shè)置路由:
router.get('/getMvt/*/*/*', (req, res, next) => {
spatial.getMvt(req, res, next);
});
然后通過(guò)解析請(qǐng)求路徑,獲取相應(yīng)的 x, y, z 的值色瘩,拼寫(xiě) SQL 語(yǔ)句伪窖,計(jì)算矢量切片:
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);
}
}
);
})
}
};
// 瓦片編號(hào)轉(zhuǎn)經(jīng)緯度
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 這兩個(gè)函數(shù),通過(guò)函數(shù) xyz2lonlat 得到相應(yīng)邊界頂點(diǎn)坐標(biāo)居兆。代碼中三個(gè)SQL語(yǔ)句分別展示了點(diǎn)、線竹伸、面的矢量切片生成方法泥栖。可以看到勋篓,可以通過(guò)多個(gè) SQL 語(yǔ)句分別對(duì)多個(gè)圖層進(jìn)行切片吧享,最后合成一個(gè)總的 SQL,實(shí)現(xiàn)多圖層的統(tǒng)一切片譬嚣。
前端代碼如下:
<!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 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>
結(jié)果
渲染結(jié)果如下:
最后钢颂,本文未考慮海量數(shù)據(jù)的性能優(yōu)化,當(dāng)縮放等級(jí)較小時(shí)拜银,請(qǐng)求數(shù)據(jù)量變大殊鞭,必然會(huì)影像性能遭垛,這時(shí)可對(duì)不同縮放等級(jí)的請(qǐng)求做不同處理,例如數(shù)據(jù)抽稀操灿、根據(jù)不同等級(jí)顯示不同屬性的數(shù)據(jù)等锯仪。
源碼地址