1 of 15

MapboxVectorTileとプロトコルバッファ

@Kanahiro

2 of 15

MapboxVectorTileとは?

  • Mapbox社が定義・開発したベクターデータの規格のひとつ
  • タイル化の概念をラスターだけでなくベクターに拡げたもの
  • 単にタイル化しているだけではなく、軽量化の様々な工夫が施されている
  • バイナリデータなので中身を見たければデコードが必要
  • プロトコルバッファと言う規格で、構造体が効率的にエンコードされている
  • https://docs.mapbox.com/vector-tiles/reference/

3 of 15

プロトコルバッファとは?

  • Googleが定義・開発したデータ構造をシリアライズ(エンコード)する仕組み[1]
  • ベクタータイルはPBFという拡張子が付される事が多いが、Protocol Bufferが由来
  • 所定の文法でデータ構造が記された.protoファイルをもとにエンコード・デコードする
  • バイナリデータなので、XMLやJSONといった文字列ベースの構造化データに対してファイルサイズ・パース速度で圧倒的に有利[2]
  • [1]https://qiita.com/yugui/items/160737021d25d761b353
  • [2]https://github.com/mapbox/pbf

4 of 15

今回やってみること

  • ベクタータイル(PBF)のデコード・エンコード・編集
    • やりたいことはPBF->構造体(Object)->編集->PBF
  • JavaScript(TypeScript)でやる
  • @mapbox/vector-tileではなく、protobuf.jsと.protoファイルでやる
    • ※@mapbox/vector-tileやvt2geojsonを使えばラクにPBF->GeoJSON変換が可能だが、GeoJSON->PBFは(たぶん)めちゃ面倒臭い

5 of 15

PBFをデコードする

import * as protobuf from 'protobufjs';

const buffer = fs.readFileSync('./13-7096-5010.pbf');

// buffer = <Buffer 1f 8b 08 ... >

const root = await protobuf.load(path.join(__dirname, 'vector_tile.proto'));

const protoVectorTile = root.lookupType('tile');

const vectorTile = protoVectorTile.decode(buffer);

const vectorTileObject = protoVectorTile.toObject(vectorTile);

/* vectorTileObject

{

layers: [

{

name: 'water',

features: [Array],

keys: [Array],

values: [Array],

extent: 4096,

version: 2

},

// 以下略

]

}

*/

6 of 15

PBFにエンコードする

import * as protobuf from 'protobufjs';

/* vectorTileObject

{

layers: [

{

name: 'water',

features: [Array],

keys: [Array],

values: [Array],

extent: 4096,

version: 2

},

// 以下略

]

}

*/

const root = await protobuf.load(path.join(__dirname, 'vector_tile.proto'));

const protoVectorTile = root.lookupType('tile');

const vectorTile = protoVectorTile.create(vectorTileObject);

const buffer = protoVectorTile.encode(vectorTile).finish();

// buffer = <Buffer 1f 8b 08 ... >

7 of 15

PBFの取り扱いまとめ

  • PBFをデコードするとObjectが得られる
  • ObjectはPBFにエンコードできる
  • つまりデコードしたObjectの内容を書き換えてからPBFに再度エンコードすればやりたい事(PBFの編集)ができる
    • オブジェクトをいじるためにはVectorTileObjectのデータ構造を理解する必要がある

8 of 15

VectorTileのデータ構造①

https://docs.mapbox.com/vector-tiles/specification/

上記ページに仕様は書いてある

属性とgeometryの持ち方がミソ

9 of 15

VectorTileのデータ構造②

interface VectorTileObject {

layers: VectorTileLayer[];

}

interface VectorTileLayer {

name: string;

keys: string[];

values?: Value[];

extent: number;

version: number;

features: VectorTileLayerFeature[];

}

interface VectorTileLayerFeature {

tags?: number[];

type: number;

geometry: number[];

}

  • VectorTileObjectとはただのlayersの配列
  • そしてlayerの中にfeaturesがある
  • ここまで聞くと、Geojsonと同じかと思ってしまうが、データの持ち方にクセがありそう単純ではない

10 of 15

VectorTileのデータ構造③

{

tags: [ 0, 0 ],

type: 3,

geometry: [

9, 8320, 127, 26, 0, 8448, 8447, 0, 0, 8447, 15, 9,

6432, 6542, 114, 1, 4, 3, 1, 1, 2, 4, 6, 15,

3, 1, 4, 6, 8, 8, 6, 10, 2, 8, 0, 2,

7, 5, 3, 4, 3, 3, 5, 15, 9, 89, 73, 258,

4, 8, 5, 2, 5, 1, 2, 4, 3, 2, 3, 10,

6, 6, 0, 8, 4, 4, 6, 0, 6, 6, 6, 2,

6, 9, 4, 4, 6, 1, 0, 5, 2, 2, 2, 18,

4, 2, 6, 8, 4, 0, 3, 13, 4, 3, 5, 11,

0, 5, 5, 2,

... 2339 more items

]

}

  • 左がGeoJSON、右が同じデータを表すVectorTile-Featureオブジェクトである
  • propertiesもgeometryも、普通のやり方では保持していないことがわかる
  • geometryはギリギリ理解できるとして、propertiesは一見どこにもない…

{

"type": "Feature",

"properties": { "mvt_id": null, "class": "ocean", "intermittent": null },

"geometry": {

"type": "MultiPolygon",

"coordinates": [

[

[

[131.835937499999972, -37.265309955618747],

[131.835937499999972, -37.230328387603876],

[131.879882812499972, -37.230328387603876],

// 中略

[131.863253116607638, -37.250408904966733],

[131.863220930099487, -37.250383284576458]

]

]

]

}

}

11 of 15

VectorTileのデータ構造④

  • geometryの詳細は省きますが、タイル化の際に、経緯度はズームレベルに応じた一定の粒度の格子点に変換されて整数値になっている(丸められてる)。なので復元ルールさえわかれば経緯度を復元できる(不可逆)。
  • propertiesはtagsに対応するが、ここから実データを復元するには、featureが含まれるlayerのデータを参照する必要がある。

{

tags: [ 0, 0 ],

type: 3,

geometry: [

9, 8320, 127, 26, 0, 8448, 8447, 0, 0, 8447, 15, 9,

6432, 6542, 114, 1, 4, 3, 1, 1, 2, 4, 6, 15,

3, 1, 4, 6, 8, 8, 6, 10, 2, 8, 0, 2,

7, 5, 3, 4, 3, 3, 5, 15, 9, 89, 73, 258,

4, 8, 5, 2, 5, 1, 2, 4, 3, 2, 3, 10,

6, 6, 0, 8, 4, 4, 6, 0, 6, 6, 6, 2,

6, 9, 4, 4, 6, 1, 0, 5, 2, 2, 2, 18,

4, 2, 6, 8, 4, 0, 3, 13, 4, 3, 5, 11,

0, 5, 5, 2,

... 2339 more items

]

}

12 of 15

VectorTileのデータ構造⑤

{

name: 'water',

features: [

{ tags: [Array], type: 3, geometry: [Array] },

{ tags: [Array], type: 3, geometry: [Array] },

{ tags: [Array], type: 3, geometry: [Array] }

],

keys: [ 'class', 'intermittent', 'brunnel' ],

values: [

{ stringValue: 'ocean' },

{ stringValue: 'lake' },

{ uintValue: [Long] }

],

extent: 4096,

version: 2

}

左はVectorTile-Layerのデータ構造

feature-propertiesの復元の鍵は、keysとvalues

たとえばtags: [ 0, 0, 0, 1 ]は以下のように復元できる(stringValueとかは一旦無視)

{ keys[0] : values[0] } -> { class: ‘ocean’ }

{ keys[0] : values[1] } -> { class: ‘lake’ }

tagsの偶数番の値は、keysのインデックスを示す

tagsの奇数番の値は、valuesのインデックスを示す

※なのでtagsの配列長は常に偶数

VectorTile-Featureの属性を編集したければ、layer, featureの関係に注意する必要がある

とくに、属性のカラムや値が既出でない場合は、layersにまず値を”登録”してから、それに応じたtagsを追加しなければならない

13 of 15

VectorTileのデータ構造⑥

Q.なんでそんなめんどくさいデータの持ち方してるの?

A.データを効率的に保持するためです。例えばレイヤーが10個あり、それぞれ地物を10個持つとします。その全てに’class’というカラムがあるとすると、100回’class’という文字列が登場します。これは効率が悪いです。’class’=0と置き換えを一度定義すれば、100回出てくるのは0という数値だけになります。

14 of 15

実際に編集してみた

実コードはかなり長いのでチラ見せ、結果は以下

タイル内の全ての地物に、{test: ‘test’}という属性を追加してみた

15 of 15

感想

  • 繰り返しを避けて効率的にデータを保持する手法がとても参考になった
  • 簡単にモジュール化できるかと思ったけど結構複雑…