---
title: 20260915_MLT使ってみた
tags:  #foss4g #オープンソース #maplibre gl js  
author: [Yohei Shiwaku](https://www.docswell.com/user/shi-works)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/VJNY9X5478.jpg?width=480
description: FOSS4G Hiroshima で学んだ知識をシェアする社内 LT 大会での発表資料です。
published: September 17, 26
canonical: https://www.docswell.com/s/shi-works/5PRQYE-2026-09-17-153126
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/VJNY9X5478.jpg)

MLT使ってみた
3次元データである震源データ494万点を
MapLibre Tile に変換して、表示してみた。
2026-09-15 FOSS4G Hiroshima 社内LT大会
塩飽 洋平


# Page. 2

![Page Image](https://bcdn.docswell.com/page/YE9P28Y4J3.jpg)

作ったもの
気象庁の地震カタログを、深さごと立体で見る
• 固定長テキストの地震月報（カタログ編）を CSV / GeoParquet / PMTiles / MLT へ変換している
• 震源を深さ方向へ配置すると、沈み込むプレートの形がそのまま見える
494万点
698km
1919年〜
無感の震源
最深の震源
収録開始
出典：気象庁 地震月報（カタログ編）震源データ
h*.dat 46ファイル・1919–2023
（利用にあたっては気象庁の利用規約に従うこと）
デモ：shiwaku.github.io/jma-earthquake-data-converter
2


# Page. 3

![Page Image](https://bcdn.docswell.com/page/GE8D586VED.jpg)

MLTとは
MVT から着想を得て、ゼロから再設計された形式
かたち
惹かれた点
MVT は地物ごとに値を並べる。MLT は表形式の列
小さくなること。公称は「大きなタイルでは最大
指向レイアウト。同じ列には似た値が集まるので
6倍の圧縮率」。そして仕様には3D 座標（標高）
、その型に合った詰め方が効く。
のサポートもあり、震源の深さをそのまま持たせ
られそうに見えた。
列ごとの「詰め方」が軽量エンコーディング。既定では一部しか有効になっていない。
仕様はいまも更新が続いている（maplibre/maplibre-tile-spec）。実装が追いついていない部分があり、今日の落とし穴はそこに由来す
る。
3


# Page. 4

![Page Image](https://bcdn.docswell.com/page/LELMYR9R7R.jpg)

作り方
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


# Page. 5

![Page Image](https://bcdn.docswell.com/page/4JMYNRZPJW.jpg)

表示のしかた
深さは属性で運び、deck.gl が立てる
① MapLibre GL JS が MLT を読む
② deck.gl が深さを足して立てる
// map/dataLayers.ts — MLT ソースの定義
// map/hypocenter3d.ts — 深さを z に組み直す
return {
const fs = map.querySourceFeatures(&#039;unfelt&#039;,
type: &#039;vector&#039;,
{ sourceLayer: &#039;unfelt&#039; })
url: blob,
// TileJSON（z/x/y.mlt）
const [lng, lat] = f.geometry.coordinates
encoding: &#039;mlt&#039;,
// ← MLT はこの1行
const km = Number(f.properties[&#039;深さ&#039;])
}
position: [lng, lat, -km * 1000]
深さはジオメトリではなく、属性で運ぶ
仕様には 3D 座標（標高）があるが JS 実装は 2D。深さは属性に入れ、deck.gl が querySourceFeatures() で拾っ
て [lng, lat, −深さ×1000] に組み直す。タイルを取りに行くのは MapLibre だけで、取得も復号も1系統。
5


# Page. 6

![Page Image](https://bcdn.docswell.com/page/PJR9DR6Y79.jpg)

踏んだ落とし穴
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


# Page. 7

![Page Image](https://bcdn.docswell.com/page/PEXQ1RM4JX.jpg)

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


# Page. 8

![Page Image](https://bcdn.docswell.com/page/3EK92RGPED.jpg)

サイズは、どうだったか 無感 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


# Page. 9

![Page Image](https://bcdn.docswell.com/page/L73W48N475.jpg)

正直なところ
形式より先に、配信設定だった
やったこと
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


# Page. 10

![Page Image](https://bcdn.docswell.com/page/87DKQMW5JG.jpg)

まとめ
試してよかった。採用はまだ保留
• 3D 座標は、仕様にはあるが JS 側が 2D。深さは属性で運んで描画側で組み立てた。デコーダに手を入れ
れば通せる。
• サイズは公称「最大6倍の圧縮率」（−83%）に届かず、既定で −29%。軽量エンコーディングで −55%。
MapLibre は問題なく読めたので、配信中のタイルも差し替えた。
• 形式より先に、配信設定だった。形式で頑張って −55%、gzip を入れるだけなら −60%。本命のデコード
速度も未計測で、採否はまだ決められない。
コードと計測値：github.com/shiwaku/jma-earthquake-data-converter
データ出典：気象庁 地震月報(カタログ編)
（利用にあたっては気象庁の利用規約に従うこと）
10


