GeoArrow のデータを Deck.gl レイヤーとして MapLibre GL JS に乗せてみる
少し前に、GeoArrow のデータを deck.gl 上に描画するための NPM パッケージがあることを知りました。
軽く調べると、
- GeoArrow のデータを deck.gl 上に描く方法(↑のパッケージ関連)
- deck.gl を MapLibre GL JS に重ねる方法
はいくつか見つかりますが、その両方をつなげたのは見つからなかったので、試してみた時のメモです。
GeoArrow とは
GeoArrow というのは、GIS データを Apache Arrow 形式で扱う際のフォーマットです。Apache Arrow は、効率的なデータ交換のために設計されたデータフォーマットです。データを保存する際の効率性ではなく、サービス間やプロセス間でのデータ交換に主眼を置いている、というのがポイントです。つまり、GeoArrow は、効率的に GIS データをやりとりするためのフォーマットです。
と言ってもピンとこないかもしれませんが、例えば、GeoJSON はデータ交換という観点ではあまり効率がよくありません。PostGIS などの DB に入っているデータを動的にウェブブラウザ上に表示するケースを考えてみましょう。GeoJSON を使う場合、DB に入っている時点ではバイナリデータだったものを一度文字列に変換して、その文字列をまたブラウザ上でパースする、という変換が発生しています。大きなデータではこうした変換がボトルネックになり得ます。そんなときは、GeoArrow を使うとパフォーマンスが改善する可能性があります。
Apache Arrow については、以下のブログ記事がコンパクトにまとまっているので、気になる方は読んでみてください。
GeoArrow のデータを MapLibre GL JS で描画するには
さて、そんなわけで GeoArrow を使いたいのですが、現状、MapLibre GL JS ではこれを描画する方法がなさそうです。データソースとして使えるタイプの一覧はこちらですが、使えそうなものがありません。addProtocol()
でむりやりなんとかできるのかもですが、基本的にはタイル用のものだと思うので違いそうです。
@geoarrow/deck.gl-layers
とは
そんな感じで、GeoArrow のデータを GPU のバッファまで無変換で届ける方法は自分で発明するしかないのか...?、と悩んでいたところ、冒頭に書いた NPM パッケージ @geoarrow/deck.gl-layers
を知りました。これは、その名の通り GeoArrow のデータを deck.gl のレイヤーとして描画するためのパッケージで、GeoArrow 公式が開発しています。
仕組みとしては、deck.gl にはバイナリデータを直接渡せる API があるので、それを使っています。ドキュメントを読んだ感じ、愚直な実装でよければ自分で実装するのもそれほど難しくなさそうです。
@geoarrow/deck.gl-layers
を使う利点は、
- ポリゴンのテッセレーション[1]をメインスレッドではなくワーカーを使って実行してくれるので、メインの処理がブロックされない。
- データが全部揃うのを待たずに、Apache Arrow の batch record 単位で非同期に処理を進めてくれる。
というあたりです。まあ、恩恵を実感するほど使い込んでいないのでどこまで効果があるかはわからないのですが、説明やコードを読む感じ、わりとありがたそうな雰囲気はあります。
deck.gl を MapLibre GL JS に重ねる
ここまでで、GeoArrow を deck.gl に描く方法は分かりましたが、その deck.gl を MapLibre に載せるにはどうすればいいのでしょうか。deck.gl は、MapLibre GL JS や Mapbox GL JS と組み合わせて使う方法を提供しています。やり方は 3 つありますが、試した感じ、MapLibre のレイヤーのひとつとして扱う(interleaved
)方法がいちばん軽そうでした。
MapLibre 側のドキュメントはこちらです(これは overlay 方式みたいです)。
@geoarrow/deck.gl-layers
を MapLibre GL JS に重ねる
@geoarrow/deck.gl-layers
は、deck.gl が提供しているレイヤーに GeoArrow
が付いたバージョンを提供しています。例えば、↑のドキュメントはこういう例になっていましたが(ScatterplotLayer
は点を描画するものです)、
import {ScatterplotLayer} from '@deck.gl/layers';
...
const deckOverlay = new MapboxOverlay({
interleaved: true,
layers: [
new ScatterplotLayer({
id: 'deckgl-circle',
data: [
{position: [0.45, 51.47]}
],
getPosition: d => d.position,
getFillColor: [255, 0, 0, 100],
getRadius: 1000,
beforeId: 'watername_ocean' // In interleaved mode render the layer under map labels
})
]
});
map.addControl(deckOverlay);
この layers
に入れるレイヤーを GeoArrowScatterplotLayer
に変えます。
- import {ScatterplotLayer} from '@deck.gl/layers';
+ import {GeoArrowScatterplotLayer} from '@geoarrow/deck.gl-layers';
...
const deckOverlay = new MapboxOverlay({
interleaved: true,
layers: [
- new ScatterplotLayer({
+ new GeoArrowScatterplotLayer({
id: 'deckgl-circle',
...
ここで、ちょっと躓いたのはオプションの指定の仕方です。data
にデータを指定するのはまあわかるんですが、get*
に指定するのは (d) => d.position
のようなアクセサの関数ではなく、data_points.getChild('geom')
のような、実際の Arrow のデータです。じゃあ data
いらなくない...?という気もするのですが、どういうルールになっているのかまだよく理解できていません。
const resp = await fetch('./points.arrow');
data_points = await arrow.tableFromIPC(resp);
...
new GeoArrowScatterplotLayer({
id: 'deckgl-circle',
data: data_points,
getPosition: data_points.getChild('geom')!,
デモ
以下が、@geoarrow/deck.gl-layers
を使って、無駄に10万個の点、1万個の線、1万個の三角形を表示したデモです[2]。
https://yutannihilation.github.io/maplibre-geoarrow-deckgl-layers/
コードはこんな感じです(svelte-maplibre-gl を使っています):
余談:DuckDB でのデータ生成
ちなみに、最終的には DuckDB WASM をブラウザで動かして、そこから GeoArrow 形式のデータを動的に生成して MapLibre GL JS上に可視化...、みたいなことをやりたいのですが、まだ DuckDB WASM の使い方がよくわからないので今回はあらかじめ生成したデータを使っていました。
若干面倒だったのは、データの型が GEOMETRY
のままだと @geoarrow/deck.gl-layers
が受け付けてくれないので、型を合わせる必要がある、という点です。どういうことかというと、例えばDuckDB の spatial extension には点を作る関数が複数ありますが(reference)、シグネチャの違いに注目してみてください。
POINT_2D ST_Point2D (x DOUBLE, y DOUBLE)
POINT_3D ST_Point3D (x DOUBLE, y DOUBLE, z DOUBLE)
GEOMETRY ST_Point (x DOUBLE, y DOUBLE)
次元の違いによって点には POINT_2D
・POINT_3D
・POINT_4D
といった型があり、ST_Point2D()
と ST_Point3D()
は戻り値がそれぞれに対応する型になっていますが、ST_Point()
は GEOMETRY
になっています。GEOMETRY
は POINT_2D
・POINT_3D
も含む上位の型で、次元が分からない場合や点・線・ポリゴンのどれになるかわからない場合はこの型が使われるようです。
DuckDB 上でやっている分にはこの違いを意識する必要はないのですが、nanoarrow extension で Apache Arrow に変換する際はこの型の違いが影響してしまいます。型が POINT_2D
型だと GeoArrow の定義と一致する List<Struct<x: double, y: double>>
で書き出されるのですが、GEOMETRY
型だと BinaryArray
になってしまいます(WKB になってる?)。
線やポリゴンの場合は、次元別の関数はないので ST_MakeLine()
や ST_MakePolygon()
の結果を LINESTRING_2D
・POLYGON_2D
にキャストしてやる必要があります。
SELECT
...
ST_MakeLine(p1, p2)::LINESTRING_2D as geom
FROM
...
ちなみに、型定義には LINESTRING_3D
や POLYGON_3D
といった型はないので、3D のデータを扱いたい場合はほんとに回避方法がなさそうです。なんとかしたい...
追記:LINESTRING_3D
と POLYGON_3D
を追加しました。
感想
DuckDB のデータ生成がぜんぜんわからなくて時間がかかったんですが、本題の部分は思ったより簡単でした。また実際に必要になったときに思い出して使ってみたいと思います。
Discussion