1. はじめに
2026-01-23 に MapLibre Tile (MLT) がリリースされ、エンコーダーで MLT を生成したり、maplibre-gl-js で表示したり、といった環境が整いました。
この記事では plateau-lod2-mvt をソースとして MVT を MLT に変換し、実際に maplibre-gl-js で表示できるかどうかを確かめてみました。
2 準備
MLT のエンコーダーは Java版と Rust版が提供されています。今回の試行では Java 版を使ってみます。
2.1 エンコーダーのビルド
こちらの手順に従って encoder.jar をビルドします。
Java 21 以上をインストールしておきましょう。Java 17 だとビルドに失敗します。
$ git clone https://github.com/maplibre/maplibre-tile-spec
$ cd maplibre-tile-spec/java
$ chmod +x gradlew
$ ./gradlew cli
$ ls -1 mlt-cli/build/libs/
decode.jar
encode.jar
mlt-cli.jar
$
encoder.jar がビルドできていたら成功です。
2.2 エンコーダーのテスト
git clone でついてくるサンプルファイルを使って、以下のように mvt-to-mlt 変換が可能です。
$ cd maplibre-tile-spec/java
$ java -jar mlt-cli/build/libs/encode.jar --mvt ../test/fixtures/simple/point-boolean.pbf --mlt /tmp/point-boolean.mlt --tessellate --outlines ALL --verbose
また、引数なし、あるいは -h or --help を付与して実行することでオプションを確認できます。
$ java -jar mlt-cli/build/libs/encode.jar --help
usage: org.maplibre.mlt.cli.Encode [--coerce-mismatch] [--colmap-auto
<columns>] [--colmap-delim <map>] [--colmap-list <map>] [--compare-all]
[--compare-geometry] [--compare-properties] [--compress <algorithm>]
[--decode] [--dir <dir>] [--elide-mismatch] [--enable-fastpfor]
[--enable-fsst] [--enable-fsst-native] [--filter-layers <arg>]
[--filter-layers-invert] [-h] [--maxzoom <level>] [--mbtiles <file>]
[--metadata] [--minzoom <level>] [--mlt <file>] [--mvt <file>]
[--noids] [--nomorton] [--offlinedb <file>] [--outlines <tables>]
[--printmlt] [--printmvt] [--rawstreams] [--regen-ids <pattern>]
[--server <port>] [--sort-ids <pattern>] [--tessellate]
[--tessellateurl <arg>] [--tilesetmetadata] [--timer] [-v <level>]
[--vectorized]
Convert an MVT tile file or MBTiles containing MVT tiles to MLT format.
Options Since Description
--mvt <file> -- Path to the input MVT file
--mbtiles <file> -- Path of the input MBTiles file.
--offlinedb <file> -- Path of the input offline database
file.
--dir <dir> -- Directory where the output is written,
using the input file basename
(OPTIONAL).
--mlt <file> -- Filename where the output will be
written. Overrides --dir.
--noids -- Do not include feature IDs.
--sort-ids <pattern> -- Reorder features of matching layers
(default all) by ID, for optimal
encoding of ID values.
--regen-ids <pattern> -- Re-generate ID values of matching
layers (default all). Sequential
values are assigned for optimal
encoding, when ID values have no
special meaning.
--filter-layers <arg> -- Filter layers by regex
--filter-layers-invert -- Invert the result of --filter-layers
--minzoom <level> -- Minimum zoom level to encode tiles.
Only applies with --mbtiles
--maxzoom <level> -- Maximum zoom level to encode tiles.
Only applies with --mbtiles
--metadata -- Write tile metadata alongside the
output tile (adding '.meta'). Only
applies with --mvt.
--coerce-mismatch -- Allow coercion of property values
--elide-mismatch -- Allow elision of mismatched property
types
--tilesetmetadata -- Write tileset metadata JSON alongside
the output tile (adding '.json'). Only
applies with --mvt.
--enable-fastpfor -- Enable FastPFOR encodings of integer
columns
--enable-fsst -- Enable FSST encodings of string columns
(Java implementation)
--enable-fsst-native -- Enable FSST encodings of string columns
(Native implementation: Not
available)
--colmap-auto <columns> -- Automatic column mapping for the
specified layers (Not implemented)
Layer specifications may be regular
expressions if surrounded by '/'.
An empty set of layers applies to all
layers.
--colmap-delim <map> -- Add a separator-based column mapping:
'[<layer>,...]<name><separator>'
e.g. '[][:]name' combines 'name' and
'name:de' on all layers.
--colmap-list <map> -- Add an explicit column mapping on the
specified layers:
'[<layer>,...]<name>,...'
e.g. '[]name,name:de' combines 'name'
and 'name:de' on all layers.
--nomorton -- Disable Morton encoding.
--tessellate -- Include tessellation data in converted
tiles.
--tessellateurl <arg> -- Use a tessellation server (implies
--tessellate).
--outlines <tables> -- The feature tables for which outlines
are included ([OPTIONAL],
comma-separated, 'ALL' for all,
default: none).
--decode -- Test decoding the tile after encoding
it. Only applies with --mvt.
--printmlt -- Print the MLT tile after encoding it.
Only applies with --mvt.
--printmvt -- Print the round-tripped MVT tile. Only
applies with --mvt.
--compare-geometry -- Assert that geometry in the decoded
tile is the same as the input tile.
Only applies with --mvt.
--compare-properties -- Assert that properties in the decoded
tile is the same as the input tile.
Only applies with --mvt.
--compare-all -- Equivalent to --compare-geometry
--compare-properties.
--vectorized -- Use the vectorized decoding path.
--rawstreams -- Dump the raw contents of the individual
streams. Only applies with --mvt.
--timer -- Print the time it takes, in ms, to
decode a tile.
--compress <algorithm> -- Compress tile data with one of
'deflate', 'gzip'. Only applies to
MBTiles and offline databases.
-v, --verbose <level> -- Enable verbose output.
-h, --help -- Show this output.
--server <port> -- Start encoding server
$
3. ファーストテイク (失敗)
3.1 MVT2MLT
変換元となる MVT を git clone して、pbf を探して mlt に変換していくだけです。
$ git clone https://github.com/indigo-lab/plateau-lod2-mvt.git
$ touch mvt2mlt.sh
$ bash mvt2mlt.sh
変換処理はこのような形に。とりあえずパラメータはチュートリアルのパラメータをそのまま流用しています。
#/bin/bash
mkdir dist
for pbf in `find plateau-lod2-mvt -name "*.pbf" | sort ` ; do
echo $pbf
dir=$(dirname dist/${pbf#*/})
mkdir -p $dir
java -jar maplibre-tile-spec/java/mlt-cli/build/libs/encode.jar --mvt $pbf --dir $dir --tessellate --outlines ALL
done
しかし、実際にこれを実行するとこんなエラーが出て異常終了してしまいます。
java.lang.RuntimeException: Layer 'bldg' Feature index 1 Property 'z' has different type: DOUBLE / INT_32
at org.maplibre.mlt.converter.MltConverter.resolveColumnType(MltConverter.java:164)
at org.maplibre.mlt.converter.MltConverter.lambda$createTilesetMetadata$0(MltConverter.java:60)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
at org.maplibre.mlt.converter.MltConverter.createTilesetMetadata(MltConverter.java:58)
at org.maplibre.mlt.cli.Encode.encodeTile(Encode.java:370)
at org.maplibre.mlt.cli.Encode.run(Encode.java:193)
at org.maplibre.mlt.cli.Encode.run(Encode.java:98)
at org.maplibre.mlt.cli.Encode.main(Encode.java:82)
3.2 分析
実際にエラーを投げているのはこのあたりのコードです。
詳細は省きますが、あるプロパティの値が Feature によって int だったり float だったりする場合にエラーが出ている、ということになります。
エンコーダー側では INT_32 を INT_64 にしたり、 FLOAT を DOUBLE にしたりといった型拡張はしてくれるのですが、 INT_32 / INT_64 を FLOAT / DOUBLE にするような型拡張には対応していないようです。
3.3 対策
MVT 側で 「プロパティ z は絶対に float でエンコード」 するように強制することができるのであれば、MVT を再生成することで対策が可能です。
tippecanoe --attribute-type
tippecanoe の --attribute-type=z:float はプロパティの型を明示的に指定するオプションですが、float を指定した場合であっても小数点以下がゼロの場合には、やはり int としてエンコードされてしまっているようでした。
あるいは、 Encoder 側で 「プロパティ z は float にキャストして処理」のようなオプションがあれば対策できそうです。
encoder.jar --coerce-mismatch
MLT Encoder の --coerce-mismatch オプションを有効にすると、型の衝突があった場合には型を String に固定する挙動となるようです。float を string として保持するのはサイズメリットが薄れてしまうので、今回のようなケースでは有効ではありません。
encoder.jar --elide-mismatch
MLT Encoder の --elide-mismatch オプションを有効にすると、型の衝突があった場合にもエラー終了せずに処理を継続してくれるようです。しかし、実際にこのように生成された MLT をブラウザで表示しようとすると、デコードエラーのためにタイル自体が非表示になったり、異常な z 値による表示の破綻など、実用に耐えない結果となりました。
現在の tippecanoe および encoder.jar ではちょうどよい対策を見出せませんでした。
若干の敗北感はありますが、 z を メートル単位の float で扱うのをあきらめて、 センチメートル単位の int とすることとしました。
4. セカンドテイク
4.1 MVT 生成
MVT を一旦 GeoJSON にデコードして z をセンチメートル単位の int となるように変換します。こんな実装です。
const vt2geojson = require("@mapbox/vt2geojson");
function processFile(file) {
return new Promise((resolve, reject) => {
const tokens = file.replace(".pbf", "").split("/");
const y = parseInt(tokens.pop());
const x = parseInt(tokens.pop());
const z = parseInt(tokens.pop());
vt2geojson(
{
uri: file,
layer: "bldg",
z: z,
x: x,
y: y,
},
function (err, result) {
if (err) reject(err);
else resolve(result);
},
);
});
}
(async function () {
for (const file of Array.from(process.argv.slice(2))) {
const geojson = await processFile(file);
for (const feature of geojson.features) {
delete feature.id;
if (feature.properties.z !== undefined)
feature.properties.z = Math.round(feature.properties.z * 100);
console.log(JSON.stringify(feature));
}
}
})();
以下のように MVT を再生成しました。
$ node fix.js plateau-lod2-mvt/*/*/*.pbf > temp.jsonl
$ cat temp.jsonl | tippecanoe --no-tile-compression -ad -an -Z10 -z16 -e mvt -l bldg -ai
4.2 MVT2MLT
再度 MVT2MLT 変換を実施します。
$ bash mvt2mlt.sh
#/bin/bash
mkdir dist
for pbf in `find mvt -name "*.pbf" | sort ` ; do
echo $pbf
dir=$(dirname dist/${pbf#*/})
mkdir -p $dir
java -jar maplibre-tile-spec/java/mlt-cli/build/libs/encode.jar --mvt $pbf --dir $dir --tessellate --outlines ALL
done
今回はエラーなしに実行が完了しました。
4.3 プレビュー
生成した MLT を使って、以下のように問題なく表示されました。
実際の HTML はこのようになっています。
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>plateau-lod2-mvt</title>
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.17.0/dist/maplibre-gl.css" />
<script src="https://unpkg.com/maplibre-gl@5.17.0/dist/maplibre-gl.js"></script>
</head>
<body>
<div id="map" style="position: absolute; top: 0; left: 0; bottom: 0; right: 0"></div>
<script>
const style = {
version: 8,
glyphs: "https://maps.gsi.go.jp/xyz/noto-jp/{fontstack}/{range}.pbf",
sources: {
pale: {
type: "raster",
tiles: ["https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png"],
minzoom: 5,
maxzoom: 18,
tileSize: 256,
attribution:
"<a href='http://maps.gsi.go.jp/development/ichiran.html'>地理院タイル</a>",
},
bldg: {
type: "vector",
tiles: [new URL("./", location.href).href + "{z}/{x}/{y}.mlt"],
minzoom: 10,
maxzoom: 16,
attribution:
"<a href='https://github.com/indigo-lab/plateau-lod2-mvt'>plateau-lod2-mvt by indigo-lab</a> (<a href='https://www.mlit.go.jp/plateau/'>国土交通省 Project PLATEAU</a> のデータを加工して作成)",
encoding: "mlt",
},
},
layers: [
{
id: "pale",
type: "raster",
source: "pale",
minzoom: 5,
maxzoom: 20,
},
{
id: "fill-extrusion-bldg",
type: "fill-extrusion",
source: "bldg",
"source-layer": "bldg",
minzoom: 10,
maxzoom: 20,
filter: ["has", "z"],
paint: {
"fill-extrusion-color": [
"interpolate-lab",
["linear"],
["%", ["round", ["*", 0.01, ["get", "z"]]], 10],
0,
"#d95d2a",
1,
"#d95d2a",
2,
"#f8df4a",
3,
"#f8df4a",
4,
"#1d508e",
5,
"#1d508e",
6,
"#3c782e",
7,
"#3c782e",
8,
"#845c40",
9,
"#726f62",
],
"fill-extrusion-height": ["*", ["get", "z"], 0.01],
},
},
],
};
new maplibregl.Map({
container: "map",
center: [139.693909, 35.686302],
hash: true,
zoom: 16,
pitch: 60,
bearing: 22,
style: style,
});
</script>
</body>
</html>
JS と CSS は maplibre-gl@5.12.0 以降のものを使用します
z の単位変更(メートル → センチメートル) を受けて各係数を調整しています
"encoding": "mlt" の追加を忘れずに
4.4 所見
ブラウザでの表示速度については MVT の場合と比較して体感できるほどの差は感じられません。他方、変換前の MVT の合計サイズが 468Mbytes, MLT が 484Mbytes と、微増という結果になりました。
MLT 変換の際に --tessellate というオプションが有効になっているのですが、これを少し追ってみましょう。
MaplibreTile 論文 では tesselation について以下のような説明があります。
A notable feature of MLT is the storage of pre-tessellated polygon
meshes directly within the file. This approach allows the computationally intensive triangulation step during runtime (online tessellation), often considered a major bottleneck in modern GPU-based map rendering, to be offloaded to the tile generation phase (offline tessellation).
一般的に WebGL は点・線・三角形しか描画できないので、ポリゴンを描画する場合にはどこかの段階で必ず三角形分割の処理が必要になります。従来これはクライアントサイドでやるしかなかったのですが (online tessellation) 、 MLT ではこれを事前に済ます offline tessellation が導入されているということです。 --tessellate はこのような offline tessellation を有効にするオプションだと推測できます。
しかし、 plateau-lod2-mvt のように fill-extrusion を想定した利用方法の場合、壁面部分のポリゴン生成と三角形分割の処理は事前にやっておくことはできず、依然としてクライアントでの処理が必要です。
encoder.jar --outlines
MLT Encoder の --outlines オプションはデフォルトのサンプルでも有効にされているのでそれを流用しています。こちらを無効にしても MLT の生成は可能なのですが、クライアントでのデコードの際に got error: Cannot convert GpuVector to coordinates without topology information" といったエラーが発生するために、今回のケースではこちらを無効にするのは得策ではないようです。
5. サードテイク
5.1 MVT2MLT
MVT2MLT 変換を実施します。 単に --tessellate を除いただけです。
$ bash mvt2mlt.sh
#/bin/bash
mkdir dist
for pbf in `find mvt -name "*.pbf" | sort ` ; do
echo $pbf
dir=$(dirname dist/${pbf#*/})
mkdir -p $dir
java -jar maplibre-tile-spec/java/mlt-cli/build/libs/encode.jar --mvt $pbf --dir $dir --outlines ALL
done
問題なく実行され、 HTML でのプレビューも機能しました。
5.2 所見
ブラウザでの表示速度については MVT、 MLT (tessellate あり) と比較して体感できるほどの差は感じられません(すこしもたつく?ような気がする、というレベル)。他方、サイズについては大きく変化がありました。
| name | size (Mbytes) |
|---|---|
| mvt | 468 |
| mlt (tessellate on) | 484 |
| mlt (tessellate off) | 279 |
MVT に対して約6割のファイルサイズに抑えられています。
6. まとめ
MVT からの MLT 生成と表示を試行してみました。
encoder.jar によるエンコード、maplibre-gl-js によるビューはいずれも問題なく機能し、テスト可能な状態であることを確認できました。エンコーダーは MVT からの変換を前提としているため、MLT が仕様上対応を謳っている三次元座標については未確認です。
MVT から MLT への変換において、プロパティとして float 値が使用されている場合には注意が必要です。今後のツールでの対応に期待したいですが、int への移行は現実的な対策です。
MLT の offline tessellate を利用した速度向上について、今回のように fill-extrusion 利用を前提としたセッティングでは効果は体感しづらかったです。大量・複雑なポリゴンを fill で表示するようなベンチマークがあるとわかりやすく効果が体感できるかもしれません。
MLT のサイズ圧縮効果について、tessallate を無効にした場合に顕著な効果がありました。全体サイズは 4割減少しています。通常、MVT (PBF) は1タイルあたり 500kbytes 程度の上限を設定して生成しますが、今後 MLTが500kbytes 程度になるように MVT 生成の上限を引き上げたり、といった調整が必要になるかもしれません。
追記
生成した MLT をこちらのレポジトリから公開しました。
(以上)

