Labo288

プログラミングのこと、GISのこと、パソコンのこと、趣味のこと

PostGISから動的にベクトルタイルを配信する

はじめに

qiita.com

上記記事では、PostGIS-backendなタイルサーバー実装比較し利用している。 いろいろ書いているが、結局のところそれらがやっていることはPostGISへのSQL実行である(ST_AsMVT)。正確にいうと、データベースのテーブルをチェックして、ジオメトリ型を含むかどうか…といったユーティリティ的な仕組みが内蔵されていて、これ自体はかなり便利である。テーブルが増減しない場合、それらのサーバーを用いるのが最も簡単・便利であろう。しかしテーブルが増減する場合は、現状それらは再起動が必要だったり、運用上厳しい面がある。

ここで、たとえば手前にWebAPIを挟んでそれらタイルサーバーに中継する…というのは純粋にオーバーヘッドで手前のサーバーから直接SQLを実行すればよいのでは、と思ったところで、本記事ではそれを試してみる。もしこれが現実的なら、動的にテーブルが増減する場合でも対応できる。

PostGISとデータの用意

PostGISpostgresql://docker:docker@localhost:5432/postgresというconnection stringで接続できるとする。また、国土数値情報行政区域データがdata.gpkg内、adminというレイヤーで格納されているとして、下記のとおりにデータをインポートする。

ogr2ogr -f PostgreSQL postgresql://docker:docker@localhost:5432/postgres data.gpkg -nln admin -progress -t_srs EPSG:3857

このときCRSをEPSG:3857としているのは、この後、タイルの領域で地物を切り出す際に簡単だからである。QGISからPostGISに接続し、レイヤーを表示してみると下記のように見える。

\d admin
                                     Table "public.admin"
 Column  |          Type          | Collation | Nullable |              Default      
---------+------------------------+-----------+----------+------------------------------------ 
 fid     | integer                |           | not null | nextval('admin_fid_seq'::regclass)
 n03_001 | character varying(10)  |           |          | 
 n03_002 | character varying(20)  |           |          | 
 n03_003 | character varying(20)  |           |          | 
 n03_004 | character varying(20)  |           |          | 
 n03_007 | character varying(5)   |           |          | 
 geom    | geometry(Polygon,3857) |           |          | 
Indexes:
    "admin_pkey" PRIMARY KEY, btree (fid)
    "admin_geom_geom_idx" gist (geom)

タイルを返すエンドポイントを実装する

Fastifyでさっくり書いてみる。テーブル名とz/x/yをパスパラメータで受けるように書く。

import fastify from 'fastify';

const { Client } = require('pg');
const client = new Client({
    user: 'docker',
    password: 'docker,
    host: 'localhost',
    database: 'postgres',
});
client.connect();

const app = fastify({
    logger: true,
});

app.get<{ Params: { tableName: string; z: number; x: number; y: number } }>(
    '/api/tiles/:tableName/:z/:x/:y',
    async (req, reply) => {
        const { tableName, x, y, z } = req.params;
        const res = client.query(
            `WITH mvtgeom AS (
                SELECT ST_AsMVTGeom(geom, ST_TileEnvelope(${z}, ${x}, ${y}), extent => 4096, buffer => 64) AS geom
                FROM ${tableName + suffix}
                WHERE geom && ST_TileEnvelope(${z}, ${x}, ${y}, margin => (64.0 / 4096))
            )
            SELECT ST_AsMVT(mvtgeom.*) FROM mvtgeom;`,
            (err: any, res: any) => {
                if (err) {
                    console.log(err.stack);
                } else {
                    reply
                        .code(200)
                        .header(
                            'Content-Disposition',
                            `attachment; filename=${y}.pbf`,
                        )
                        .send(res.rows[0].st_asmvt);
                }
            },
        );
    },
);

app.listen(8000, '0.0.0.0', (err) => {
    if (err) {
        console.error(err);
        process.exit(1);
    }
});

ここで、SQL内の手順的には①当該タイルに含まれる地物を抽出(ST_TileEnvelope)②タイル上のジオメトリに変換(ST_AsMVTGeom)③タイルをバイナリデータへ変換(ST_AsMVT)となっている。

上記のAPIからタイルを取得する。adminというタイルの場合、ベクトルタイルのURLテンプレートはhttp://localhost:8000/api/tiles/admin/{z}/{x}/{y}となる。QGISで接続してみる。

ベクトルタイルを表示出来た。PostGISレイヤーの場合よりも明らかにレスポンス速度は向上している。ただしベクトルタイルといえど、日本列島を全て覆うようなタイルはサイズが大きくなり、レスポンスは遅くなる。要求パフォーマンスに対してリクエストするズームレベルを調整する必要があるだろう。

終わりに

結局のところST_AsMVTがイケてて高速なので、ベクトルタイルサーバーに頼らずともPostGISから動的にタイルを配信することはできる。ただし実運用では手前にキャッシュサーバーなどを配置すべきだろう。また、この仕組みがあっても、tippecanoeやOpenMapTIlesなどで生成したタイルを静的に配信する仕組みを完全に置き換えるものではない。