ピクセルパーフェクトなバイナリベクトルタイルを作ってみる

# はじめに

とある事情で **タイルの全領域をひとつのポリゴンが過不足なくカバーするようなバイナリベクトルタイル (pbf)** が必要になりました。

# 1. GeoJSON と tippecanoe で

こんな GeoJSON を用意して

```bounds.json
{
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "properties": {},
    "geometry": {
      "type": "Polygon",
      "coordinates": [
        [
          [-180, -90],
          [180, -90],
          [180, 90],
          [-180, 90],
          [-180, -90]
        ]
      ]
    }
  }]
}
```

こんなかんじで tippecanoe を実行しました。

```bash
$ tippecanoe --no-tile-compression -z0 -Z0 -e dist -X -l polygon fill.json
1 features, 39 bytes of geometry, 9 bytes of separate metadata, 0 bytes of string pool
  76.9%  0/0/0  
$ find dist
dist
dist/metadata.json
dist/0
dist/0/0
dist/0/0/0.pbf
$ base64 dist/0/0/0.pbf 
GjB4AgoHcG9seWdvbiiAIBIgGAMiHAmgQZ8BOgDAQp8BAP8/AJ8BAAC/QqABAIBAAA8=
$ 
```

ここで `0/0/0.pbf` が欲しかった pbf です。

これを以下のような HTML で表示してみます。
地理院タイル(写真) と data スキームの URI として表現されるバイナリベクトルタイルをソースとして、
写真を背景に半透明ネイビーの塗りとホワイトの枠線が表示される、ということを意図しています。


```html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <title>data scheme MVT</title>
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
  <link href="https://unpkg.com/maplibre-gl@1.15.2/dist/maplibre-gl.css" rel="stylesheet" />
  <script src="https://unpkg.com/maplibre-gl@1.15.2/dist/maplibre-gl.js"></script>
</head>

<body style="margin:0;padding:0;">
  <div id="map" style="position:absolute;top:0;left:0;bottom:0;right:0;"></div>
  <script>
    new maplibregl.Map({
      container: 'map',
      zoom: 17,
      maxZoom: 24,
      center: [138.906883, 35.173874],
      hash: true,
      style: {
        "version": 8,
        "glyphs": "https://maps.gsi.go.jp/xyz/noto-jp/{fontstack}/{range}.pbf",
        "sources": {
          "ortho": {
            "type": "raster",
            "tiles": [
              "https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg"
            ],
            "tileSize": 256,
            "minzoom": 2,
            "maxzoom": 18,
            "attribution": "<a href='https://maps.gsi.go.jp/development/ichiran.html'>地理院タイル</a>"
          },
          "grid": {
            "type": "vector",
            "tiles": [
              "data:application/vnd.mapbox-vector-tile;base64,GjB4AgoHcG9seWdvbiiAIBIgGAMiHAmgQZ8BOgDAQp8BAP8/AJ8BAAC/QqABAIBAAA8="
            ],
            "minzoom": 2,
            "maxzoom": 18
          }
        },
        "layers": [{
          "id": "ortho",
          "type": "raster",
          "source": "ortho",
          "minzoom": 2,
          "maxzoom": 24,
          "layout": {
            "visibility": "visible"
          }
        }, {
          "id": "grid-fill",
          "type": "fill",
          "source": "grid",
          "source-layer": "polygon",
          "minzoom": 2,
          "maxzoom": 24,
          "paint": {
            "fill-color": "navy",
            "fill-opacity": 0.5
          }
        }, {
          "id": "grid-line",
          "type": "line",
          "source": "grid",
          "source-layer": "polygon",
          "minzoom": 2,
          "maxzoom": 24,
          "paint": {
            "line-color": "red",
            "line-width": 1
          },
          "layout": {
            "visibility": "visible"
          }
        }]
      }
    });
  </script>

</body>

</html>

```



実際の表示はこちら。
全体的にネイビーが表示されていますが、枠線は表示されていませんね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/86eeaae9-e41d-0652-6b82-fbc7fbe609a4.png)

比較のために以下のように小さくした GeoJSON をソースにすると

```json
{
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "properties": {},
    "geometry": {
      "type": "Polygon",
      "coordinates": [
        [
          [-90, -45],
          [90, -45],
          [90, 45],
          [-90, 45],
          [-90, -45]
        ]
      ]
    }
  }]
}
```

`GiR4AgoHcG9seWdvbiiAIBIUGAMiEAmAMIIXGgD6Ef8fAAD5EQ8=` のような base64 が得られ、
これで HTML を表示したものがこちら。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/18c58999-0d6b-807d-fd19-1192d2d00ae3.png)

ポリゴンがタイル領域の内側に含まれていれば、枠線は表示されるようですね。

当初の目的、**タイルの全領域をひとつのポリゴンが過不足なくカバーするようなバイナリベクトルタイル (pbf)** を達成するためには適当な緯度経度、たとえば [このへん](https://en.wikipedia.org/wiki/Web_Mercator_projection) から引っ張ってきた `[-180,-85.051129,180,85.051129]` とか、[このへん](https://epsg.io/3857) から引っ張ってきた`[-180.0,-85.06,180.0,85.06]` のような緯度経度範囲を指定してやれば良さそうなものですが、そもそも最初に提示した `[-180,-90,180,90]` の事例で縦方向の枠線が表示されていないので、longitude=180 がタイルの外側とみなされているのかもしれません。

トライ＆エラーも良いのですが、Mapbox Vector Tile 自体は内部で座標を整数値として管理しています。ピクセルパーフェクトを目指して厳密にやってみましょう。

# 2. Protocol Buffer で

[Vector Tile Specification](https://github.com/mapbox/vector-tile-spec/tree/master/2.1) に従って、手作業でエンコードしてみます。

最終的にはこのようになりました。ソースを貼っておきます。

なおソース中の `vector_tile.js` は <https://www.npmjs.com/package/pbf> に従って <https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto> から生成したものです。

```js
const Pbf = require("pbf");
const Tile = require('./vector_tile.js').Tile;

const encode = value => (value << 1) ^ (value >> 31);

const extent = 4096;
const offset = 0;
for (let offset = -16; offset <= 16; offset++) {

  const n = extent - offset * 2;

  const c = {
    "layers": [{
      "version": 2,
      "name": "polygon",
      "features": [{
        "id": 0,
        "tags": [],
        "type": 3,
        "geometry": [
          9, // [00001-001] command id 1 : moveTo,  command count 1
          encode(offset),
          encode(offset),
          26, // [00011-010] command id 2 : lineTo, command count 3
          encode(0),
          encode(n),
          encode(n),
          encode(0),
          encode(0),
          encode(-n),
          15 // [00001-111] command id 7 : closePath
        ]
      }],
      "keys": [],
      "values": [],
      "extent": extent
    }]
  };

  const out = new Pbf();
  Tile.write(c, out);
  const bin = out.finish();
  console.log(extent, offset, Buffer.from(bin).toString("base64"));

}
```

実行結果はこちら。

```bash
$ node main.js 
4096 -16 GiJ4AgoHcG9seWdvbhISGAMiDgkfHxoAwEDAQAAAv0APKIAg
4096 -15 GiJ4AgoHcG9seWdvbhISGAMiDgkdHRoAvEC8QAAAu0APKIAg
4096 -14 GiJ4AgoHcG9seWdvbhISGAMiDgkbGxoAuEC4QAAAt0APKIAg
4096 -13 GiJ4AgoHcG9seWdvbhISGAMiDgkZGRoAtEC0QAAAs0APKIAg
4096 -12 GiJ4AgoHcG9seWdvbhISGAMiDgkXFxoAsECwQAAAr0APKIAg
4096 -11 GiJ4AgoHcG9seWdvbhISGAMiDgkVFRoArECsQAAAq0APKIAg
4096 -10 GiJ4AgoHcG9seWdvbhISGAMiDgkTExoAqECoQAAAp0APKIAg
4096 -9 GiJ4AgoHcG9seWdvbhISGAMiDgkRERoApECkQAAAo0APKIAg
4096 -8 GiJ4AgoHcG9seWdvbhISGAMiDgkPDxoAoECgQAAAn0APKIAg
4096 -7 GiJ4AgoHcG9seWdvbhISGAMiDgkNDRoAnECcQAAAm0APKIAg
4096 -6 GiJ4AgoHcG9seWdvbhISGAMiDgkLCxoAmECYQAAAl0APKIAg
4096 -5 GiJ4AgoHcG9seWdvbhISGAMiDgkJCRoAlECUQAAAk0APKIAg
4096 -4 GiJ4AgoHcG9seWdvbhISGAMiDgkHBxoAkECQQAAAj0APKIAg
4096 -3 GiJ4AgoHcG9seWdvbhISGAMiDgkFBRoAjECMQAAAi0APKIAg
4096 -2 GiJ4AgoHcG9seWdvbhISGAMiDgkDAxoAiECIQAAAh0APKIAg
4096 -1 GiJ4AgoHcG9seWdvbhISGAMiDgkBARoAhECEQAAAg0APKIAg
4096 0 GiJ4AgoHcG9seWdvbhISGAMiDgkAABoAgECAQAAA/z8PKIAg
4096 1 GiJ4AgoHcG9seWdvbhISGAMiDgkCAhoA/D/8PwAA+z8PKIAg
4096 2 GiJ4AgoHcG9seWdvbhISGAMiDgkEBBoA+D/4PwAA9z8PKIAg
4096 3 GiJ4AgoHcG9seWdvbhISGAMiDgkGBhoA9D/0PwAA8z8PKIAg
4096 4 GiJ4AgoHcG9seWdvbhISGAMiDgkICBoA8D/wPwAA7z8PKIAg
4096 5 GiJ4AgoHcG9seWdvbhISGAMiDgkKChoA7D/sPwAA6z8PKIAg
4096 6 GiJ4AgoHcG9seWdvbhISGAMiDgkMDBoA6D/oPwAA5z8PKIAg
4096 7 GiJ4AgoHcG9seWdvbhISGAMiDgkODhoA5D/kPwAA4z8PKIAg
4096 8 GiJ4AgoHcG9seWdvbhISGAMiDgkQEBoA4D/gPwAA3z8PKIAg
4096 9 GiJ4AgoHcG9seWdvbhISGAMiDgkSEhoA3D/cPwAA2z8PKIAg
4096 10 GiJ4AgoHcG9seWdvbhISGAMiDgkUFBoA2D/YPwAA1z8PKIAg
4096 11 GiJ4AgoHcG9seWdvbhISGAMiDgkWFhoA1D/UPwAA0z8PKIAg
4096 12 GiJ4AgoHcG9seWdvbhISGAMiDgkYGBoA0D/QPwAAzz8PKIAg
4096 13 GiJ4AgoHcG9seWdvbhISGAMiDgkaGhoAzD/MPwAAyz8PKIAg
4096 14 GiJ4AgoHcG9seWdvbhISGAMiDgkcHBoAyD/IPwAAxz8PKIAg
4096 15 GiJ4AgoHcG9seWdvbhISGAMiDgkeHhoAxD/EPwAAwz8PKIAg
4096 16 GiJ4AgoHcG9seWdvbhISGAMiDgkgIBoAwD/APwAAvz8PKIAg
$ 

```

`extent, offset, base64-encoded-pbf` のようなレイアウトになっています。

offset=0 のものを使って HTML で表示した結果がこちら。ちゃんと枠線が出ましたね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/cf78d164-6db0-2701-5090-63ac6060ca16.png)


offset=8 の場合。ちょっとだけ隙間ができて、タイルごとに枠線が描画されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/2cd50b3b-b962-cffc-a105-5c492fe2cb48.png)

offset=16 とするともっと隙間が大きくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/0a31fb8f-ff9c-cb42-acca-c8e7e0cce858.png)


offset=-8 の場合。枠線は表示されなくなりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/4ea1ded1-ea47-b29d-7442-64f04a5765a5.png)


# まとめ

この記事は完全に私的な備忘録、といったところですが、もしもなんらかの事情で **ピクセルパーフェクトな pbf** が必要になったときや **タイルの全領域をひとつのポリゴンが過不足なくカバーするようなバイナリベクトルタイル (pbf)** が必要になったときにどうぞ。


あと、単にタイル境界を表示したいということであればこちらのオプションを使うのがはるかに手軽です。
<https://docs.mapbox.com/mapbox-gl-js/api/map/#map#showtileboundaries>


# 追記

ブラウザツール化しました

<https://frogcat.github.io/mvt-grid-builder/>


[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/62b09c2e-4743-faa2-2d5a-f4a406407e37.png)](https://frogcat.github.io/mvt-grid-builder/)