-- Views
September 17, 26
スライド概要
FOSS4G Hiroshima で学んだ知識をシェアする社内 LT 大会での発表資料です。
イベント発表資料の公開
MLT使ってみた 3次元データである震源データ494万点を MapLibre Tile に変換して、表示してみた。 2026-09-15 FOSS4G Hiroshima 社内LT大会 塩飽 洋平
作ったもの 気象庁の地震カタログを、深さごと立体で見る • 固定長テキストの地震月報(カタログ編)を CSV / GeoParquet / PMTiles / MLT へ変換している • 震源を深さ方向へ配置すると、沈み込むプレートの形がそのまま見える 494万点 698km 1919年〜 無感の震源 最深の震源 収録開始 出典:気象庁 地震月報(カタログ編)震源データ h*.dat 46ファイル・1919–2023 (利用にあたっては気象庁の利用規約に従うこと) デモ:shiwaku.github.io/jma-earthquake-data-converter 2
MLTとは MVT から着想を得て、ゼロから再設計された形式 かたち 惹かれた点 MVT は地物ごとに値を並べる。MLT は表形式の列 小さくなること。公称は「大きなタイルでは最大 指向レイアウト。同じ列には似た値が集まるので 6倍の圧縮率」。そして仕様には3D 座標(標高) 、その型に合った詰め方が効く。 のサポートもあり、震源の深さをそのまま持たせ られそうに見えた。 列ごとの「詰め方」が軽量エンコーディング。既定では一部しか有効になっていない。 仕様はいまも更新が続いている(maplibre/maplibre-tile-spec)。実装が追いついていない部分があり、今日の落とし穴はそこに由来す る。 3
作り方 CSV から MLT まで、4段 CSV GeoJSONSeq MVT MLT z/x/y.mlt 震源カタログ 行区切りGeoJSON tippecanoe encode.jar ファイルに展開 494万行 csv2geojsonseq.py mbtilesで出す mbtilesで返る ビューワが直接読む mbtiles は途中の入れ物で、配信はしない。エンコーダが mbtiles を受け取って mbtiles を返すので、最後に 17,833 枚のファイルへ展開 する。MapLibre は HTTP 越しに mbtiles を読めないため。 リポジトリでは build_unfelt_mlt_tiles.sh の1本にまとまっている。 4
表示のしかた
深さは属性で運び、deck.gl が立てる
① MapLibre GL JS が MLT を読む
② deck.gl が深さを足して立てる
// map/dataLayers.ts — MLT ソースの定義
// map/hypocenter3d.ts — 深さを z に組み直す
return {
const fs = map.querySourceFeatures('unfelt',
type: 'vector',
{ sourceLayer: 'unfelt' })
url: blob,
// TileJSON(z/x/y.mlt)
const [lng, lat] = f.geometry.coordinates
encoding: 'mlt',
// ← MLT はこの1行
const km = Number(f.properties['深さ'])
}
position: [lng, lat, -km * 1000]
深さはジオメトリではなく、属性で運ぶ
仕様には 3D 座標(標高)があるが JS 実装は 2D。深さは属性に入れ、deck.gl が querySourceFeatures() で拾っ
て [lng, lat, −深さ×1000] に組み直す。タイルを取りに行くのは MapLibre だけで、取得も復号も1系統。
5
踏んだ落とし穴 4つ。どれも実装がまだ追いついていないところ • 01 エンコーダは、タイルを作ってくれない 参考実装の Encode CLI は MVT を入力に取るトランスコーダ。先 に tippecanoe で MVT を作る。--no-tile-compression を忘れると落ちる。 • 02 Java 21 以上が要る 配布物がないので ./gradlew cli で自前ビルド。手元の 17 では通らなかった。 • 03 同じ列に INT と DOUBLE が混ざると、止まる 深さ 135 は INT、135.5 は DOUBLE。書き出しで +0.0001 し て全行 float に寄せ、表示側で丸め直した。 • 04 軽量エンコーディング(SIMD-FastPFOR + FSST)が、既定でオフ 既定で有効なのは座標のモートンだけ 。整数列の SIMD-FastPFOR と文字列列の FSST 辞書エンコーディングは仕様上まだ experimental で、Java 実装は 両方 false。ただし MapLibre GL JS 6.9.0 は問題なく読めた。 4つの再現手順とコードは github.com/shiwaku/jma-earthquake-data-converter にある。 6
3D 座標は、どうだったか 仕様にはあるが、JS 側が 2D だった • エンコーダの Position が [number, number] • 仕様の Vec3(VertexBufferType.VEC_3)は enum に値があるだけで参照0件 • MapLibre の loadGeometry() が new Point(x, y) で z を捨てる 回避は5枚目で見せたとおり。参考実装 gsi-cyberjapan/gsi-3d-2025 が建物の標高で採っている構成と同じ。 この先 とはいえ、デコーダに手を入れれば 3D は通せるはず。仕様側に 3D 座標がある以上、足りないのは実装だ けだと分かる。 7
サイズは、どうだったか 無感 4,938,455件 / z0–10 / 17,833タイル 公称「最大6倍の圧縮率」に対し、 既定 −29%、軽量エンコーディングで −55% 198.9MB 140.3MB 89.1MB MVT 非圧縮 MLT 既定 MLT 軽量エンコーディング 配信中 基準 −29% −55% ←いま • %はすべて MVT 非圧縮 198.9MB からの削減率。既定で −29%、軽量エンコーディングを有効にすると −55%。同じ GeoJSONSeq から作った MVT を変換したので、変数は形式だけ。 • 公称の条件に揃えても届かない。大きいタイル上位10枚だけで測っても −59%。そして gzip をかけると差 はもっと縮む。gzip 同士で比べると MVT 79.1MB に対し MLT 60.6MB、−23% にとどまる。 公称は「列指向レイアウトと軽量エンコーディングで、大きなタイルでは最大6倍の圧縮率」。6倍は「元の1/6になる」なので −83%。そ の条件に揃えて大きい順の上位10枚だけで測っても −59%(全体は −55%)で、ほとんど変わらない。届かないのはタイルの大きさでは なく、属性5列の点データという中身のため。 8
正直なところ 形式より先に、配信設定だった やったこと MVT 非圧縮 198.9 MB 出発点 MLT 既定 140.3 MB 形式を替えた MLT 軽量エンコーディング 89.1 MB さらにエンコーディングを詰めた MVT + gzip 79.1 MB 形式は替えず、設定を1つ入れただけ 前日まで配信していたもの 差し替えて、いま配信中 これをやっていなかった 転送量の削減 — −29% −55% −60% つまり 形式を替え、軽量エンコーディングまで入れて 89.1MB。それでも元の MVT を gzip しただけの 79.1MB に負けて いる。形式より先に、設定だった。 両方やれば 59.1MB(−70%)。いまも非圧縮で配っていて、そこは宿題。 9
まとめ 試してよかった。採用はまだ保留 • 3D 座標は、仕様にはあるが JS 側が 2D。深さは属性で運んで描画側で組み立てた。デコーダに手を入れ れば通せる。 • サイズは公称「最大6倍の圧縮率」(−83%)に届かず、既定で −29%。軽量エンコーディングで −55%。 MapLibre は問題なく読めたので、配信中のタイルも差し替えた。 • 形式より先に、配信設定だった。形式で頑張って −55%、gzip を入れるだけなら −60%。本命のデコード 速度も未計測で、採否はまだ決められない。 コードと計測値:github.com/shiwaku/jma-earthquake-data-converter データ出典:気象庁 地震月報(カタログ編) (利用にあたっては気象庁の利用規約に従うこと) 10