バイナリベクトルタイルを地図ライブラリ以外で扱う

[Mapbox GL JS] や [Leaflet] といった地図ライブラリを使わずに JavaScript でバイナリベクトルタイルを使う方法です。

## やること

`fetch` で `pbf (mvt)` 形式のファイルを取得し、Features の配列に変換する。

## コード

npm で [pbf] と [@mapbox/vector-tile] モジュールをインストールします。

```Shell
$ npm install --save pbf @mapbox/vector-tile
```

`fetch` して response → ArrayBuffer → Pbf の順に変換し、GeoJSON Feature の配列を取得

```JavaScript
import Pbf from 'pbf';
import { VectorTile } from '@mapbox/vector-tile';

const tileCoord = { x: 14582, y: 6414, z: 14 };
const baseUrl = 'https://cieloazul310.github.io/mvt-tiles/tile/population_250m/';
const layerName = '250mesh';

fetch(baseUrl + `${tileCoord.z}/${tileCoord.x}/${tileCoord.y}.mvt`)
  .then(res => res.arrayBuffer())
  .then(buffer => new Pbf(buffer))
  .then(pbf => {
    const layer = new VectorTile(pbf).layers[layerName];
    const features = [];
    if (layer) {
      for (let i = 0; i < layer.length; i++) {
        const feature = layer.feature(i).toGeoJSON(tileCoord.x, tileCoord.y, tileCoord.z);
        features.push(feature);
      }
    }
    return features;
  })
  .then(features => {
    console.log(features);
    // => Array(46) [ feature, feature, feature, ..., feature ]
  });
```

バイナリベクトルタイルの拡張子は作成時に mvt にしていたので、この記事でも mvt としています。  
しかし、この分野の国内における先駆者である[@hfu]さんの記事 [バイナリベクトルタイルの拡張子が mvt であったり pbf であったりするのはなんでだろう] に下記のようなコメントが付いていたので、今後は pbf にします。

> その後、Mapzen (Tilezen) のサービスが（いったん）終了して状況が変化したので、hfu も拡張子を pbf に変更しました。  

## 応用

`fetchTile` という関数を作成します。

この関数はタイル座標のオブジェクト `tile` を入力し、Promise オブジェクト(fetch)を返します。  
`fetch` が成功したら mvt (pbf) 形式のファイルの変換を行ない、GeoJSON Feature の配列 features を返し、失敗したら空の配列を返します。

```JavaScript
import Pbf from 'pbf';
import { VectorTile } from '@mapbox/vector-tile';

/**
  * @param {object} tile 
  * @returns {Promise} Promise object
  */ 
function fetchTile(tile) {
  return fetch(baseUrl + `${tile.z}/${tile.x}/${tile.y}.mvt`)
  .then(res => res.arrayBuffer())
  .then(buffer => new Pbf(buffer))
  .then(pbf => {
    const layer = new VectorTile(pbf).layers[layerName];
    const features = [];
    if (layer) {
      for (let i = 0; i < layer.length; i++) {
        const feature = layer.feature(i).toGeoJSON(tileCoord.x, tileCoord.y, tileCoord.z);
        features.push(feature);
      }
    }
    return features;
  })
  .catch(() => []);
}
```

[d3-tile] などで求めたタイル座標の組の配列 `tiles` から、`array.map` で Promise オブジェクトの配列 `tasks` を生成します。  
Promise オブジェクトの配列 `tasks` を生成することで、 `Promise.all(tasks)` で全てのタイルの取得が終わった後に、一括で処理を行なうことができます。

```JavaScript
const tiles = [{ x, y, z }, { x, y, z }, ..., { x, y, z }];
const tasks = tiles.map(tile => fetchTile(tile));
// => Array [ Promise, Promise, ..., Promise ]

Promise.all(tasks)
  .then(data => {
    console.log(data);
    // => Array [ features, [], features, features, [], ...]
    
    // 二次元配列である data を一次元の配列 features に変換
    const features = data.reduce(
      (accum, curr) => [...accum, ...curr], 
      []
    );

    return features;
    // => Array [ feature, feature, ..., feature ]
  });
```

[国内のサッカースタジアム周辺の人口を求める]という記事では、上記の方法で人口データの入ったバイナリベクトルタイルを取得し、[Turf.js] を使って距離円との演算を行いました。

## 参照

- [pbf] / GitHub
- [@mapbox/vector-tile] / GitHub
- [Protocol Buffers] / Google Developers
- [Array Buffer] / MDN web docs

<!-- References -->
[水戸地図]: https://cieloazul310.github.io
[twitter]: https://twitter.com/cieloazul310
[Qiita]: https://qiita.com/cieloazul310

[Mapbox GL JS]: https://www.mapbox.com/mapbox-gl-js/api/
[D3.js]: https://d3js.org/
[Leaflet]: https://leafletjs.com/

[地理院地図]: https://maps.gsi.go.jp/

[国内のサッカースタジアム周辺の人口を求める]: https://cieloazul310.github.io/2018/04/stadium-and-population/
[Turf.js]: http://turfjs.org/
<!-- Google Developers -->
[Protocol Buffers]: https://developers.google.com/protocol-buffers/
<!-- MDN web docs -->
[Array Buffer]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
<!-- GitHub -->
[pbf]: https://github.com/mapbox/pbf
[@mapbox/vector-tile]: https://github.com/mapbox/vector-tile-js
[d3-tile]: https://github.com/d3/d3-tile
<!-- Qiita -->
[@hfu]: https://qiita.com/hfu
[バイナリベクトルタイルの拡張子が mvt であったり pbf であったりするのはなんでだろう]: https://qiita.com/hfu/items/69fbe24be92654f634da