TypeScriptでベクトルタイルを形成する

Posted by keita-kobayashi on April 21, 2022 · 2 mins read

Geoloniaでは地理空間データを作ったり変形したりした上、ほとんどの場合は表示するためにベクトルタイルとして配信します。一般的にベクトルタイルを作るには元のデータから各ズームレベルのベクトルタイルを予め生成ツールを使います。例えば、GeoJSONからベクトルタイルを作りたい場合は tippecanoe を使ったり、 Postgis を使う場合は ST_AsMVT という関数を使います。また、OpenStreetMapのデータをベクトルタイルとして配信するケースも多々ありますが、こちらは専用のツールを使います。Geolonia では tilemaker を使っておりますが、他にも OpenMapTiles もあります。

今回は、あらかじめタイルを作るという用途よりは上流のデータをもとに動的に作る必要があったので、今まで使ったツールでは不十分でした。Mapbox Vector Tile形式protobuf でエンコードされた情報なので、もしエンコーディングなど自分でできれば、ベクトルタイル専用のライブラリとか使わずに、そのまま Mapbox Vector Tile の protobuf 定義を使って自力で作ることができます。

タイルを作りましょう

ここからの説明は TypeScript の基本的な設定が行なっていることを前提としています。

まずは、 Mapbox Vector Tile の protobuf 定義を組み込みます。私は git submodule で組み込みましたが、 vector_tile.proto のファイルさえあれば大丈夫です。

git submodule add https://github.com/mapbox/vector-tile-spec.git

そして、こちらのファイルを読み込んでエンコーティングするためのコードや TypeScript の定義を作るコマンドを package.json に登録します。

"scripts": {
  ...
  "build:protobuf": "pbjs -t static-module -w es6 -o src/libs/protobuf.js vector-tile-spec/2.1/vector_tile.proto && pbts -o src/libs/protobuf.d.ts src/libs/protobuf.js"
}

このコマンドに pbjspbts というものを呼び出していますが、 protobufjs パッケージに入っています。

npm install --save protobufjs

さて、コードを生成しましょう。

npm run build:protobuf

生成後、 src/libs/protobuf.jssrc/libs/protobuf.d.ts に結果が入っています。

Mapbox Vector Tile の内容説明

MapboxのドキュメンテーションにMVTの内容について説明するページがあります。今回特に重要な点をリストします:

  • Feature のプロパティー名や値は重複排除し、レイヤーごとの配列に保存します。参照する場合は、こちらの配列のインデックスを使います。
  • 座標の値は ZigZag エンコーディングを使います。
  • 点・線・ポリゴンの書き方はいくつかの点で GeoJSON と違います。
    • GeoJSONは点・線・ポリゴンは宣言型ですが、MVTは命令型です。
    • GeoJSONは各点は絶対の座標を使いますが、MVTはカーソルを使い、最後の命令からの相対座標を使います。
    • GeoJSONの座標は WGS84 (EPSG:4326) で表現していますが、MVTの座標は EPSG:3857 に投影した後の該当タイルの相対座標で表現します。相対座標の範囲は extent というパラメータで決められます。

さて、基本情報が揃ったのでコードを書いてみましょう。

import { vector_tile } from "../libs/protobuf";

// ZigZag エンコーディング
const zz = (value: number) => (value << 1) ^ (value >> 31);

const tile = vector_tile.Tile.create({
  layers: [
    {
      version: 2, // 個体値
      name: "test", // レイヤー名
      extent: 256, // このタイルの相対座標の範囲を指定する。256に設定すると、このタイルが256ピクセル四角で作られていることを示します。 256×256=65536 ピクセル。範囲は: (0,0) から (256,256) まで
      features: [
        {
          // レイヤー内、idがユニークである必要があります。
          id: 1,
          type: vector_tile.Tile.GeomType.POLYGON,
          geometry: [
            // (0,0) で始めます
            ((1 & 0x7) | (1 << 3)), // MoveTo (1) 命令を 1 回実行
              zz(5), zz(5), // (5,5) に移動
            ((2 & 0x7) | (3 << 3)), // LineTo (2) 命令を 3 回実行
              zz(1),  zz(0), // 線を (5,5) から (6,5) まで引く
              zz(0),  zz(1), // 線を (6,5) から (6,6) まで引く
              zz(-1), zz(0), // 線を (6,6) から (5,6) まで引く
            15, // パスを閉じる(暗黙的に最後の点 (5,6) から最初の点 (5,5) に線を引きます)
          ],
          tags: [
            0, // test-property-key-1
            0, // value of key 1

            1, // test-property-key-2
            1, // value of key 2 and 3

            2, // test-property-key-3
            1, // value of key 2 and 3
          ],
        }
      ],
      keys: [
        "test-property-key-1",
        "test-property-key-2",
        "test-property-key-3"
      ],
      values: [
        {stringValue: "value of key 1"},
        {stringValue: "value of key 2 and 3"},
      ],
    }
  ]
});

この Feature はどの形を作るかわかりますか?

こちらのタイルをバイナリに変換するには:

const buffer: Buffer = vector_tile.Tile.encode(tile).finish();

ここからそのままファイルに保存したり、 Lambda を使う場合は return { body: buffer.toString('base64'), isBase64Encoded: true } などで配信できます。

おわりに

当初、ベクトルタイルがブラックボックスであり、必ず他のツールを使って生成したりGeoJSONに変換してからデバッグや、QGISで開いたりしましたが、今回実際作ってみたら慣れた後(最初、絶対・相対の変換計算や命令の相対座標変換とか結構ハマりました。。)は意外とスムーズに行けました。



Geolonia では、ウェブ地図や位置情報を利用したウェブアプリケーションや、モバイルアプリケーションの開発を承っています。

お問い合わせ