はじめに
前回の記事 Leaflet.VectorGrid でバイナリベクトルタイルを表示する では leaflet.VectorGrid.Protobuf というプラグインをつかってバイナリベクトルタイルを表示してみました。
ただ、正直ブラックボックスで中で何をやっているかよくわかりません。この記事では理解のために
- ベクトルタイル取得
- バイナリから GeoJSON を読み出し
- GeoJSON をレイヤーとして Leaflet に追加
という手順を実行するカスタムプラグインを作ることで、中でどんなことが行われているかを追ってみたいと思います。
やってみた
こんなコードが
<!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>
こうなりました
ワーキングデモはこちらから。
解説
ライブラリ
さすがにバイナリのパースを自分でやるのは大変なので、ライブラリの力を借ります。前回記事でも少し触れたのですが、@mapbox/vector-tile と 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 要素) を返すことになっているので、これを実装しています。詳しくは このあたり。
メインの処理
本丸の処理ですが結構短いです。
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');
}
-
createTile
メソッドはcoords
オブジェクトを引数として呼び出されます。coords
の中身は{z:15,x:29299,y:12071}
のようにタイルの xyz 座標を持っています。 - 続いて
L.Util.template(url,coords)
によって、バイナリベクトルタイルの取得先の URL を作ります - 作った URL からデータを取得します。jquery でも生の XHR でもなんでもいいのですがここでは
fetch(url)
を使っています。最終的にarrayBuffer
が欲しいので途中でa.arrayBuffer()
を使っていることに注意。 - うけとった
arrayBuffer
からnew Pbf(buffer)
によって、Pbf
のインスタンスを作ります - さらに
Pbf
からnew VectorTile(pbf)
によってVectorTile
のインスタンスを作ります -
VectorTile
のインスタンスにはlayers
というプロパティがあり、これはレイヤー名
をキー、VectorTileLayer
オブジェクトをバリューとしたオブジェクトです -
VectorTileLayer
オブジェクトはVectorTileFeature
オブジェクトをたくさんもった配列のようなものです。保持数はlength
に格納されており、VectorTileLayer.feature(index)
メソッドによってVectorTileFeature
を得ることができます。 -
VectorTileFeature
にはtoGeoJSON
というメソッドがあり、このメソッドを実行することで GeoJSON を得ることができます。ただし、feature.toGeoJSON(x,y,z)
のように、冒頭のタイル座標を渡す必要があることに注意が必要です。 - ここまでで GeoJSON が得られたので
L.geoJson(geojson).addTo(map)
によって雑にレイヤー化して地図に追加します
とりあえず押さえておくべきは VectorTile
VectorTileLayer
VectorTileFeature
の3つのクラスぐらいで、https://github.com/mapbox/vector-tile-js の API ガイドを読めば十分という印象でした。
まとめ
ベクトルタイルを取得して GeoJSON 化して Leaflet に追加する処理を自分で書いてみました。実はあんまり難しい部分はなくて、割と短いステップでできたことに驚きです。もちろん普段は VectorGrid なんかをそのまま使えばいいと思いますが、トラブルシューティングなんかの際にも役にたつかもしれません。