Leaflet でバイナリベクトルタイル処理の流れを追ってみる

# はじめに

前回の記事 [Leaflet.VectorGrid でバイナリベクトルタイルを表示する](https://qiita.com/frogcat/items/36e9e7525537c748f952) では leaflet.VectorGrid.Protobuf というプラグインをつかってバイナリベクトルタイルを表示してみました。

ただ、正直ブラックボックスで中で何をやっているかよくわかりません。この記事では理解のために

- ベクトルタイル取得
- バイナリから GeoJSON を読み出し
- GeoJSON をレイヤーとして Leaflet に追加

という手順を実行するカスタムプラグインを作ることで、中でどんなことが行われているかを追ってみたいと思います。

# やってみた

こんなコードが

```index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Hello MVT</title>
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css" />
  <script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
  <script src="https://unpkg.com/leaflet.vectorgrid@1.3.0/dist/Leaflet.VectorGrid.bundled.min.js"></script>
</head>

<body>
  <div id="map" style="position:absolute;top:0;left:0;bottom:0;right:0;"></div>
  <script>
    var MyGridLayer = L.GridLayer.extend({
      createTile: function(coords) {
        var template = "https://wata909.github.io/atsuma_mvt/sediment/{z}/{x}/{y}.pbf";
        fetch(L.Util.template(template, coords)).then(a => a.arrayBuffer()).then(buffer => {
          var layers = new VectorTile(new Pbf(buffer)).layers;
          Object.keys(layers).forEach(name => {
            var layer = layers[name];
            for (var i = 0; i < layer.length; i++) {
              var geojson = layer.feature(i).toGeoJSON(coords.x, coords.y, coords.z);
              L.geoJson(geojson).addTo(map);
            }
          });
        });
        return document.createElement('div');
      }
    });

    var map = L.map("map", {
      maxZoom: 19,
      zoom: 15,
      center: [42.7450, 141.9123],
    });

    L.tileLayer('https://maps.gsi.go.jp/xyz/ort/{z}/{x}/{y}.jpg', {
      attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>"
    }).addTo(map);

    new MyGridLayer({
      attribution: "<a href='https://github.com/koukita/2018_09_06_atumatyou' target='_blank'>この地図は地理院地図　平成30年北海道胆振東部地震 厚真川地区 正射画像をトレースした地図です</a>"
    }).addTo(map);
  </script>
</body>

</html>


```

こうなりました

![image.png](https://qiita-image-store.s3.amazonaws.com/0/82494/fba4fe16-4512-3b10-ac69-16909d14bf02.png)

ワーキングデモはこちらから。

- <https://bl.ocks.org/frogcat/raw/4dc69e64d065f1dd3a773378eeb88b98/>

# 解説

## ライブラリ

さすがにバイナリのパースを自分でやるのは大変なので、ライブラリの力を借ります。前回記事でも少し触れたのですが、[@mapbox/vector-tile](https://www.npmjs.com/package/@mapbox/vector-tile) と [pbf](https://www.npmjs.com/package/pbf) があればとりあえず大丈夫です。

ただ、こいつらは基本的には node.js 用なので webpack なり browserify なりしないといけないので面倒です。実は <https://unpkg.com/leaflet.vectorgrid@1.3.0/dist/Leaflet.VectorGrid.bundled.min.js> にはこのへんのライブラリがバンドルされていて使えるようになっています。今回は VectorGrid は使いませんが、バンドルライブラリを使わせてもらいましょう。

`@mapbox/vector-tile` が `VectorTile` という名前で、`pbf` が `Pbf` という名前でバンドルされています。

## L.GridLayer

Leaflet の L.GridLayer を拡張して作りました。
唯一実装が必要なメソッド `createTile` は xyz を入力としてタイル (任意の HTML 要素) を返すことになっているので、これを実装しています。詳しくは [このあたり](https://leafletjs.com/reference-1.3.4.html#gridlayer)。

## メインの処理

本丸の処理ですが結構短いです。

```js
createTile: function(coords) {
  var template = "https://wata909.github.io/atsuma_mvt/sediment/{z}/{x}/{y}.pbf";
  fetch(L.Util.template(template, coords)).then(a => a.arrayBuffer()).then(buffer => {
    var layers = new VectorTile(new Pbf(buffer)).layers;
    Object.keys(layers).forEach(name => {
      var layer = layers[name];
      for (var i = 0; i < layer.length; i++) {
        var geojson = layer.feature(i).toGeoJSON(coords.x, coords.y, coords.z);
        L.geoJson(geojson).addTo(map);
      }
    });
  });
  return document.createElement('div');
}
```

1. `createTile` メソッドは `coords` オブジェクトを引数として呼び出されます。`coords` の中身は `{z:15,x:29299,y:12071}` のようにタイルの xyz 座標を持っています。
2. 続いて `L.Util.template(url,coords)` によって、バイナリベクトルタイルの取得先の URL を作ります
3. 作った URL からデータを取得します。jquery でも生の XHR でもなんでもいいのですがここでは `fetch(url)` を使っています。最終的に `arrayBuffer` が欲しいので途中で `a.arrayBuffer()` を使っていることに注意。
4. うけとった `arrayBuffer` から `new Pbf(buffer)` によって、 `Pbf` のインスタンスを作ります
5. さらに `Pbf` から `new VectorTile(pbf)` によって `VectorTile` のインスタンスを作ります
6. `VectorTile` のインスタンスには `layers` というプロパティがあり、これは `レイヤー名` をキー、 `VectorTileLayer` オブジェクトをバリューとしたオブジェクトです
7. `VectorTileLayer` オブジェクトは `VectorTileFeature` オブジェクトをたくさんもった配列のようなものです。保持数は `length` に格納されており、`VectorTileLayer.feature(index)` メソッドによって `VectorTileFeature` を得ることができます。
8. `VectorTileFeature` には `toGeoJSON` というメソッドがあり、このメソッドを実行することで GeoJSON を得ることができます。ただし、`feature.toGeoJSON(x,y,z)` のように、冒頭のタイル座標を渡す必要があることに注意が必要です。
9. ここまでで GeoJSON が得られたので `L.geoJson(geojson).addTo(map)` によって雑にレイヤー化して地図に追加します

とりあえず押さえておくべきは `VectorTile` `VectorTileLayer` `VectorTileFeature` の３つのクラスぐらいで、<https://github.com/mapbox/vector-tile-js> の API ガイドを読めば十分という印象でした。

# まとめ

ベクトルタイルを取得して GeoJSON 化して Leaflet に追加する処理を自分で書いてみました。実はあんまり難しい部分はなくて、割と短いステップでできたことに驚きです。もちろん普段は VectorGrid なんかをそのまま使えばいいと思いますが、トラブルシューティングなんかの際にも役にたつかもしれません。