Node.js
geo

ベクトルタイルの生産、計測、ホスティング

画像タイルよりも速いベクトルタイルを

基本地図のベクトルタイルが主流化する最も重要な条件は、画像タイルよりも速いベクトルタイルが生産・消費可能になることだと思います。ベクトルタイルは、主流化しない限りは贅沢品になってしまうので、ベクトルタイルをやるからには主流化を追求しなければなりません。

画像タイルよりも速いベクトルタイルが生産可能であることを実証することに絞って、いくつかの作業を行ってみました。

まずは速いベクトルタイルが作れることを確証して、それからスタイルなどを考える方が良いでしょう。スタイルが重要ではないというわけでは決してなく、スタイル以前の問題があった場合に、大事なスタイルの話が根底から崩れることを事前に回避するためです。

問題の定義

画像タイルよりも速いこと、それが要求性能です。これを要求仕様に変換することにして、ざっくりと「1タイル128KBまで」と要求仕様を決めてしまいます。この要求仕様で要求性能が出やすくするために、まずはホスティング環境を整理し、次に要求仕様が満たされていることを計測する指標をつくり、最後に、問題の分割統治に留意しながら、生産を行うことにしました。

ホスティング

主要な要求仕様を「1タイル128KBまで」と定めたのですが、その大きさのタイルで十分な性能が出るためには、まずはそのタイルをホストするサーバの性能が十分であることを担保するのが良いと考えました。

tile-block

私がサーバとして扱って良い機体は Linux であり、できるだけ OS 環境を汚さないほうが良いと考えました。また、今回の作業は画像タイルよりも速いベクトルタイルが生産可能であることを実証することに目的を絞っており、本運用のサーバとの関係を考慮する必要はないと考えました。

これらの考察から、今回は、Node.js で express ベースの、HTTP/2 および CORS が設定された、.mbtiles ファイルから直接ベクトルタイルを供給する、小規模なサーバを自分で書くのがよいと考えました。

npm の express, spdy, cors および @mapbox/mbtiles を使用した70行程度のスクリプトで、必要なサーバを実装することができました。

ファイルシステム上のベクトルタイルを HTTP/1.1 で提供するナイーブなサーバをそれまで使っていましたが、それと比べると安心できる性能を出せるサーバとなりました。

エンジンについて一応の信頼が置けるものを入手したところで、本題であるベクトルタイルの設計に向けて、要求しようが満たされていることを確認する計測手段の開発に入りました。

計測

mc

量レベル q と、それを使ったズームレベルごとのタイルのサイズ分布の指標である qmatrix については、2017年のベクトルタイルアドベントカレンダーでも書き損ねていたところですが、この qmatrix の算出を、.mbtiles ファイルを対象に行うプログラムである mc を書いています。

これも、Node.js で 50行程度の小さなコードです。どのズームレベルに、どのくらいのサイズのベクトルタイルが存在するのか、一画面に収まる数字の行列 qmatrix で知ることができます。

この qmatrix を観測することにより、どのズームレベルのタイルがオーバーサイズであるのか把握でき、ソースデータへのズームレベルの割り当てを自信を持って調整できるようになりました。

mbstat

mc では、タイルのサイズの分布はわかりますが、ではオーバーサイズのタイルの中にどのレイヤのデータが詰まっているのかは、知ることができません。そこで、それを把握するための 40 行強の Node.js スクリプトを作りました。

これを使うことで、都市域に集中している建築物ポリゴンがオーバーサイズタイルを作ることが理解でき、建築物ポリゴンをそれまでのz=14からz=16に思い切って飛ばすことができました。これにより、(.mbtiles ファイルが5倍程度は大きくなることと引き換えに、)要求仕様を簡単にクリアすることができるようになりました。

ベクトルタイルの生産

ベクトルタイルの生産にあたっては、これまで使用の経験がある tippecanoe を信頼することにしました。tippecanoe にあまりいちどきに巨大なデータを与えると安定性または性能のペナルティがある可能性があると思いましたので、z=5 タイルの領域で世界をモジュールに分割し、マスターデータベースからは(国ごとではなくて)z=5 モジュールごとに切り出しを行いました(pnd)。tippecanoe で作成した .mbtiles データを、z=5 モジュールを考慮しながらはり合わせるツールである mbstitch でつなぎ合わせ、大面積のベクトルタイルセットを安定的に生産できるようにしています。

pnd

PostGIS データベースから、z=5 モジュール単位で GeoJSON の NDJSON として地物を切り出すツールです。z=5 モジュール矩形での切り出しには、turf を使っています。これも、60行程度の Node.js スクリプトになっています。

mbstitch

pnd の出力を tippecanoe で処理して得られた mbtiles は、z=5 モジュール単位であり、しかも z=5 モジュールの外部に多少のゴミを抱えています。ゴミを拾わないようにしつつ、コマンドラインパラメータで与えられた mbtiles を一つにまとめるスクリプトを作成しました。これは 66行の Node.js スクリプトになっています。

@mapbox/mbtiles と sqlite3 を使ってしまうと、出力側も入力側もコールバックになってしまい、プログラムの書き方が非常に苦しくなります。そこで、入力側は better-sqlite3 を使うことにし、row を整理したものをジェネレータ関数から yield するようにして、@mapbox/mbtiles のコールバックにしっかりと 1:1 で付き合うように書いています。このあたり、Node.js の sqlite3 の設計は、better-sqlite3 の作者の方がおっしゃる通り、必ずしも最適ではないのでしょう。

既存のツールを yet another という感じでつなぎ直すような作業になりましたが、こういったあたりのノウハウを改めて身につけるとともに、JavaScript 進歩の現在のタイミングで、実装をモダナイズするというのは、意外と価値のあることかもしれないとも思っています。

生産実績

アフリカ全土をカバーする、OSM データ全部を含む z=16 までのベクトルタイルを、z=5 のモジュールのセットとして、一勤務日で実施することができました。(モジュール群を mbstitch で組み上げる処理については、夜間バッチとなっています。)この速度はなかなか高速であると考えています。たとえば、OpenMapTiles でアフリカ全土を作るときと比べても、軽速でよい生産実績であると認識しています。

なぜ OpenMapTiles を生産に使わなかったか

OpenMapTiles は問題の定義にあたっての学習や、対照とするタイルセットの生産に非常に重宝しましたが、私の場合は次の理由で生産に使いませんでした。

  1. z=14 のベクトルタイルが重すぎる。
  2. 自分が使用する環境では、良い PostGIS データベースがあり、それを前提としてベクトルタイルの生産を考えることができる。OpenMapTiles は PostGIS と一体化しすぎており、それが私のユースケースには合わなかった。
  3. 上記2.と非常に近い問題であるが、OpenMapTiles の場合にはスキーマが Yaml と SQL で定義されている。性能の良いベクトルタイルを出すためには、さらには多種のデータを混ぜて使うためには、スキーマを柔軟に変更して行くことが必要であるが、OpenMapTiles のスキーマの表現は、やや複雑すぎると感じた。
  4. openmaptiles.com のビジネスモデル上、自然な話としてまったく理解できる話であるが、やや知的所有権の扱いに制約を感じた。より高い自由度が欲しかった。
  5. 生産の性能が出にくいアーキテクチャであり、問題の分割の仕方も、上記 pnd で行ったようなタイルモジュールベースのものにするべきだと思った。

結果的に、OpenMapTiles からはよく学びつつ、どちらかというと Mapbox が公開している Node.js のツールや tippecanoe を、よりモダナイズした形で運用するような仕事をしたことになりました。