使用 PostGIS 生成矢量瓦片
文章背景与核心概要
本指南详细介绍了如何利用 PostGIS 从 Postgres 数据库中直接编程生成 Mapbox 矢量瓦片(Mvt),并结合 MapLibre GL 与 Supabase JS 在网页端实现高效渲染。通过引入 Overture Maps Foundation 的开放地图数据,你将学会如何下载地理数据集、将其导入 Supabase、进行坐标转换,并在按需获取丰富元数据的同时,实时提供矢量瓦片服务。
这种方法充分发挥了空间数据库的强大计算能力,显著减少了前端的网络传输负载,为构建高性能、可交互的地理空间应用开辟了新途径。
目录
- 使用 Overture Maps 下载开放地图数据
- 将 GeoJSON 转换为 SQL
- 将位置数据导入 Supabase
- 启用 RLS 并创建公共读取策略
- 使用 PostGIS 生成矢量瓦片
- 使用 supabase-js 从 MapLibre GL 客户端获取矢量瓦片
- 按需获取额外的 JSON 元数据
- 总结
- 更多 Supabase 资源
Overture Maps Foundation 是由亚马逊、Meta、微软和 TomTom 联合发起的项目,旨在创建可靠、易于使用且可互操作的开放地图数据。
借助 Overture Maps,我们可以将开放地图数据(例如兴趣点)下载为 GeoJSON 格式,将其转换为 SQL,并导入到 Supabase 上的 Postgres 数据库中。然后,通过 PostGIS,我们可以编程生成矢量瓦片,并通过 supabase-js 将其提供给 MapLibre GL 客户端。
[!NOTE] 什么是矢量瓦片? 矢量瓦片是将地理数据打包成预定义、近似正方形的“瓦片”数据包,以便在网络上传输。客户端请求地图数据时,只需请求对应特定大小和位置的正方形区域的一组瓦片。
特别是对于大型数据集,这极大地减少了数据传输量,因为只需获取当前视口和当前缩放级别内的数据。
你将学到什么
- 使用 Overture Maps 下载 GeoJSON 格式的开放地图位置数据。
- 使用 GDAL (
ogr2ogr) 将 GeoJSON 转换为 SQL 语句。 - 使用
psql将位置数据和 JSON 元数据导入 Supabase Postgres 数据库。 - 使用 PostGIS 的
ST_AsMVT将对应瓦片图层的一组行聚合成二进制矢量瓦片表示。 - 使用 MapLibre 的
addProtocol通过supabase-js发起远程过程调用(RPC),可视化大型 PostGIS 表。 - 使用
supabase-js按需获取额外的 JSON 元数据。
使用 Overture Maps 下载开放地图数据
Overture Maps 提供了一个 Python 命令行工具,用于下载感兴趣区域内的数据并将其转换为几种常见的地理空间文件格式。
运行以下命令将新加坡的地点下载到 GeoJSON 文件中:
overturemaps download --bbox=103.570233,1.125077,104.115855,1.490957 -f geojson --type=place -o places.geojson
(注意:根据边界框的大小,此下载可能需要一些时间。)
将 GeoJSON 转换为 SQL
接下来,使用 GDAL ogr2ogr 将 GeoJSON 文件转换为与 PostGIS 兼容的 SQL 文件。
你可以通过 Homebrew 安装 GDAL(brew install gdal)或按照官方下载说明进行安装。
PG_USE_COPY=true ogr2ogr -f pgdump places.sql places.geojson
将位置数据导入 Supabase
在 Supabase 数据库中专门的 gis 架构(schema)内启用 PostGIS 扩展。你可以通过导航到 SQL 编辑器 并运行以下脚本来完成此操作,也可以从 数据库扩展设置 中管理扩展。
建议: 由于 PostGIS 的计算开销较大,我们建议在专用的架构(例如命名为
gis)中启用它。
CREATE SCHEMA IF NOT EXISTS "gis";
CREATE EXTENSION IF NOT EXISTS "postgis" WITH SCHEMA "gis";
通过命令行将开放地图数据导入 Supabase 中的 places 表:
psql -h aws-0-us-west-1.pooler.supabase.com -p 5432 -d postgres -U postgres.project-ref < places.sql
(你可以在 Supabase 仪表板的项目连接页面上找到你的连接凭据。)
启用 RLS 并创建公共读取策略
为了使地点数据可供公开查询,请配置行级安全性(RLS)策略以启用公共读取访问权限。在 Supabase SQL 编辑器 中运行以下代码:
ALTER TABLE "public"."places" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Enable read access for all users"
ON "public"."places"
FOR SELECT
USING (true);
使用 PostGIS 生成矢量瓦片
为了在客户端请求时编程生成矢量瓦片,需要创建一个可以通过远程过程调用 (RPC) 调用的 Postgres 函数。
在你的 SQL 编辑器中运行此脚本:
CREATE OR REPLACE FUNCTION mvt(z integer, x integer, y integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
mvt_output text;
BEGIN
WITH
-- Define the bounds of the tile using the provided Z, X, Y coordinates
bounds AS (
SELECT ST_TileEnvelope(z, x, y) AS geom
),
-- Transform geometries from EPSG:4326 to EPSG:3857 and clip to bounds
mvtgeom AS (
SELECT
-- Include name and ID only at zoom > 13 to keep low-zoom tiles lightweight
CASE
WHEN z > 13 THEN id
ELSE NULL
END AS id,
CASE
WHEN z > 13 THEN names::json->>'primary'
ELSE NULL
END AS primary_name,
categories::json->>'main' as main_category,
ST_AsMVTGeom(
ST_Transform(wkb_geometry, 3857), -- Transform to Web Mercator
bounds.geom,
4096, -- Tile extent in pixels (commonly 256 or 4096)
0, -- Buffer around the tile in pixels
true -- Clip geometries to the tile extent
) AS geom
FROM
places, bounds
WHERE
ST_Intersects(ST_Transform(wkb_geometry, 3857), bounds.geom)
)
-- Generate the MVT from the clipped geometries and encode as base64
SELECT INTO mvt_output encode(ST_AsMVT(mvtgeom, 'places', 4096, 'geom'), 'base64')
FROM mvtgeom;
RETURN mvt_output;
END;
$$;
使用 supabase-js 从 MapLibre GL 客户端获取矢量瓦片
你可以在 GitHub 上找到 index.html 的完整源代码。以下是如何在 MapLibre GL 中注册自定义协议,以便通过 supabase-js 获取 base64 编码的二进制矢量瓦片:
const client = supabase.createClient('your-supabase-api-url', 'your-supabase-anon-key')
function base64ToArrayBuffer(base64) {
var binaryString = atob(base64)
var bytes = new Uint8Array(binaryString.length)
for (var i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes
}
maplibregl.addProtocol('supabase', async (params, abortController) => {
const re = new RegExp(/supabase:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/)
const result = params.url.match(re)
const { data, error } = await client.rpc('mvt', {
z: result[2],
x: result[3],
y: result[4],
})
const encoded = base64ToArrayBuffer(data)
if (!error) {
return { data: encoded }
} else {
throw new Error(`Tile fetch error`)
}
})
注册了自定义协议后,将其实际添加到 MapLibre GL 的数据源(sources)中——例如,叠加在来自 Protomaps 的底图之上:
const map = new maplibregl.Map({
hash: true,
container: 'map',
style: {
version: 8,
glyphs: 'https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf',
sources: {
supabase: {
type: 'vector',
tiles: ['supabase://boston/{z}/{x}/{y}'],
attribution: '© <a href="https://overturemaps.org">Overture Maps Foundation</a>',
},
protomaps: {
type: 'vector',
url: 'https://api.protomaps.com/tiles/v3.json?key=your-protomaps-api-key',
attribution: 'Basemap © <a href="https://openstreetmap.org">OpenStreetMap</a>',
},
},
},
})
按需获取额外的 JSON 元数据
为了减小网络负载大小,我们避免将繁重的元数据直接嵌入到矢量瓦片中。相反,我们配置了一个 onclick 处理程序,当用户打开 MapLibre GL 弹出窗口(popup)时异步获取额外的元数据:
const popup = new maplibregl.Popup({
closeButton: true,
closeOnClick: false,
maxWidth: 'none',
})
function loadDetails(element, id) {
element.innerHTML = 'loading...'
client
.from('places')
.select(`
websites,
socials,
phones,
addresses,
source: sources->0->dataset
`)
.eq('id', id)
.single()
.then(({ data, error }) => {
if (error) return console.error(error)
element.parentElement.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`
})
}
map.on('click', 'overture-pois-text', async (e) => {
if (e.features.length > 0) {
const feature = e.features[0]
popup.setHTML(`
<table style="font-size:12px">
<tr>
<td>id:</td>
<td>${feature.properties.id}</td>
</tr>
<tr>
<td>name:</td>
<td>${feature.properties.primary_name}</td>
</tr>
<tr>
<td>main_category:</td>
<td>${feature.properties.main_category}</td>
</tr>
<tr>
<td>details:</td>
<td>
<span onclick="loadDetails(this, '${feature.properties.id}')">
load details
</span>
</td>
</tr>
</table>
`)
popup.setLngLat(e.lngLat)
popup.addTo(map)
}
})
总结
PostGIS 异常强大,它使你能够直接从存储在 Postgres 中的表行编程生成矢量瓦片。结合 Supabase 自动生成的 API 以及 supabase-js 客户端库,构建高性能、可交互的地理空间应用程序从未如此简单。