🎃

ベクトルタイルを作成する方法

2023/10/13に公開

はじめに

maplibreなどjavascriptの地図ライブラリで読み込めるベクトルタイルを作成する方法をC#で行う方法とPostGISで行う方法の2パターン紹介します

下準備

PostGISの設置

簡単にdocker-composeを使用してPostGISを用意します

services:
  postgis:
    image: postgis/postgis
    environment:
      POSTGRES_DB: vector_tile_example
      POSTGRES_USER: example
      POSTGRES_PASSWORD: example
    ports:
      - "5432:5432"

テストデータ作成

ツールでPostGISに接続し、下記のSQLを実行しサンプルデータを作成します

create table 
    points (
        id serial, 
        name varchar(100), 
        location geometry, 
        primary key (id)
    );
insert into 
    points 
    (
        name, 
        location
    ) 
    values
        ('東京駅', st_geomfromtext('POINT(139.76769903733918 35.68139335470638)', 4326)),
        ('東京タワー', st_geomfromtext('POINT(139.74540410167575 35.658607404547354)', 4326)),
        ('東京スカイツリー', st_geomfromtext('POINT(139.81069924696237 35.710000524374095)', 4326))

プロジェクト作成と必要パッケージのインストール

いずれの方法にせよPostgreSQLを扱うパッケージが必要なので、Nugetでインストールします
Npgsql

以上で準備は完了です

C#でベクトルタイルを作成する方法

処理の流れとしては、データベースで空間検索を行い取得したデータを元にベクトルタイルを作成します
この方法のメリットはPostGIS以外のデータベース(SQLServerやMySQLなど)でもベクトルタイルが作成可能な点です

必要なパッケージ

Nugetで以下のパッケージをインストールします
NetTopologySuite.IO.VectorTiles.Mapbox

処理詳細

かなり大雑把な例として下記のようなアクションを作成します

[Route("tile/test1/{z}/{x}/{y}")]
public IActionResult Test1(int x, int y, int z)
{
    var tile = new Tile(x, y, z);
    var vectorTile = new VectorTile(){ TileId = tile.Id };
    var layer = new Layer(){ Name = "points"};
        
    using var connection = new NpgsqlConnection("server=localhost;port=5432;username=example;password=example;database=vector_tile_example");
    connection.Open();
    using var cmd = connection.CreateCommand();
    cmd.CommandText =
        "SELECT ST_ASTEXT(location) AS geom, name FROM points WHERE ST_INTERSECTS(location, ST_TRANSFORM(ST_TILEENVELOPE(@z, @x, @y), 4326))";
    cmd.Parameters.Add(new NpgsqlParameter("x", x));
    cmd.Parameters.Add(new NpgsqlParameter("y", y));
    cmd.Parameters.Add(new NpgsqlParameter("z", z));
        
    var wktReader = new WKTReader();
    using var reader = cmd.ExecuteReader();
    while (reader.Read())
    {
        var geom = wktReader.Read((string)reader["geom"]);
        var attribute = new AttributesTable(new Dictionary<string, object>()
        {
            { "name", (string)reader["name"] }
        });
        layer.Features.Add(new Feature(geom, attribute));
    }
    vectorTile.Layers.Add(layer);

    using var ms = new MemoryStream();
    vectorTile.Write(ms);

    return File(ms.ToArray(), "application/pbf", $"{z}.{x}.{y}.pbf");

}

最初のこの部分でベクトルタイルのインスタンスを作成します
ここでレイヤー名をpointsとしていますが、実際にmaplibreから読み込むときにこの名前が必要となります

var tile = new Tile(x, y, z);
var vectorTile = new VectorTile(){ TileId = tile.Id };
var layer = new Layer(){ Name = "points"};

次にこの部分では、タイルに含まれるデータを空間検索しています
ここでポイントとなるのはST_TILEENVELOPEです
x,y,zで矩形のポリゴンを作成してくれます
ただし、SRIDが3857なので今回のケースではST_TRANSFORMで4326に変換する必要があります

using var connection = new NpgsqlConnection("server=localhost;port=5432;username=example;password=example;database=vector_tile_example");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText =
    "SELECT ST_ASTEXT(location) AS geom, name FROM points WHERE ST_INTERSECTS(location, ST_TRANSFORM(ST_TILEENVELOPE(@z, @x, @y), 4326))";
cmd.Parameters.Add(new NpgsqlParameter("x", x));
cmd.Parameters.Add(new NpgsqlParameter("y", y));
cmd.Parameters.Add(new NpgsqlParameter("z", z));
        
var wktReader = new WKTReader();
using var reader = cmd.ExecuteReader();

最後に取得したデータからGeometryを作成しFeatureを作成し、前段で作成したlayerに追加していきます
バイナリとして出力すれば完了です
今回はGeometry型のカラムのlocationを一度ST_ASTEXTで文字列として取得し、再度WKTReaderでGeometryに復元するという二度手間のような処理をしていますが、EntityFrameworkなどを使用し直接Geometryとして取得できる場合はこの処理は不要です

while (reader.Read())
{
    var geom = wktReader.Read((string)reader["geom"]);
    var attribute = new AttributesTable(new Dictionary<string, object>()
    {
        { "name", (string)reader["name"] }
    });
    layer.Features.Add(new Feature(geom, attribute));
}
vectorTile.Layers.Add(layer);

using var ms = new MemoryStream();
vectorTile.Write(ms);

return File(ms.ToArray(), "application/pbf", $"{z}.{x}.{y}.pbf");

PostGISでベクトルタイルを作成する方法

SQLクエリだけで完結するのでC#で作成する場合よりも簡潔です
追加で必要なパッケージもありません

処理詳細

C#の場合と同様、かなり大雑把な例として下記のようなアクションを作成します

[Route("tile/test2/{z}/{x}/{y}")]
public IActionResult Test2(int x, int y, int z)
{
    using var connection = new NpgsqlConnection("server=localhost;port=5432;username=example;password=example;database=vector_tile_example");
    connection.Open();
    using var cmd = connection.CreateCommand();
    cmd.CommandText =
        "SELECT ST_AsMvt(t.*, 'points') FROM (SELECT ST_AsMVTGeom(location, ST_TRANSFORM(ST_TILEENVELOPE(@z, @x, @y), 4326)) AS geom, name FROM points WHERE ST_INTERSECTS(location, ST_TRANSFORM(ST_TILEENVELOPE(@z, @x, @y), 4326))) AS t";
    cmd.Parameters.Add(new NpgsqlParameter("x", x));
    cmd.Parameters.Add(new NpgsqlParameter("y", y));
    cmd.Parameters.Add(new NpgsqlParameter("z", z));
        
    using var reader = cmd.ExecuteReader();
    var data = new List<byte>();
    while (reader.Read())
    {
        if (reader[0] is byte[] b)
        {
            data.AddRange(b);
        }
    }
    return File(data.ToArray(), "application/pbf", $"{z}.{x}.{y}.pbf");
}

データベースから取得したデータを結合してクライアントに返します
ポイントとなるのはSQLクエリのみです

SELECT 
    ST_AsMvt(t.*, 'points') 
FROM
(
    SELECT 
        ST_AsMVTGeom(location, ST_TRANSFORM(ST_TILEENVELOPE(@z, @x, @y), 4326)) AS geom,
        name 
    FROM 
        points 
    WHERE 
        ST_INTERSECTS(location, ST_TRANSFORM(ST_TILEENVELOPE(@z, @x, @y), 4326))
) AS t

空間検索(where句)の部分はC#の場合と全く同じです
ポイントは ST_AsMVTGeomST_AsMvtです
ST_AsMVTGeomでタイルでGeometryを切り抜きしつつベクトルタイルの空間座標に変換します
その後ST_AsMvtでベクトルタイルとして読み込めるバイナリに変換します
第2引数のpointsはレイヤー名です

動作確認

簡単なHTMLを作成します
C#版を表示するようにしています
コメントアウトを外してPostGIS版を確認することもできます

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js"></script>
    <link href="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css" rel="stylesheet" />
</head>
<body>
<div id="map" style="width: 500px; height: 500px"></div>
<script>
    var map = new maplibregl.Map({
        container: 'map',
        style: {
            version: 8,
            sources: {
                bg: {
                    type: 'raster',
                    tiles: ['https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png'],
                    tileSize: 256,
                    attribution:
                            '<a href="http://www.gsi.go.jp/kikakuchousei/kikakuchousei40182.html" target="_blank">地理院タイル</a>',
                },
                csharp: {
                    type: 'vector',
                    tiles: ['http://localhost:5269/tile/test1/{z}/{x}/{y}']
                },
                postgis: {
                    type: 'vector',
                    tiles: ['http://localhost:5269/tile/test2/{z}/{x}/{y}']
                }
            },
            layers: [
                {
                    id: 'bg',
                    type: 'raster',
                    source: 'bg',
                    minzoom: 0,
                    maxzoom: 18,
                },
                // C#版
                {
                    id: 'csharp',
                    type: 'circle',
                    source: 'csharp',
                    'source-layer': 'points',
                    minzoom: 0,
                    maxzoom: 18,
                    paint:{
                        'circle-color': '#BB0000',
                        'circle-radius': 10
                    }
                },
                // PostGIS版
                /*{
                    id: 'postgis',
                    type: 'circle',
                    source: 'postgis',
                    'source-layer': 'points',
                    minzoom: 0,
                    maxzoom: 18,
                    paint:{
                        'circle-color': '#BB0000',
                        'circle-radius': 10
                    }
                },*/
            ],
        },
        center: [139.76769903733918, 35.68139335470638], // starting position [lng, lat]
        zoom: 9 // starting zoom
    });
</script>
</body>
</html>

最後に

C#でベクトルタイルを作成する場合はPostGISでなくても可能です
またGeoJSONなどテキストファイルでも可能です
一方、PostGISで作成する場合はPHPやRubyなどC#以外の言語でも可能です
状況に合わせて使い分けることが可能です

今回のソースはGitHubに置いてあります
https://github.com/yMori2022/vector-tile-example

Discussion