0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MapLibre Tile のパフォーマンスについて

0
Posted at

はじめに

ベクトルタイル形式として長年使われてきた Mapbox Vector Tile (MVT) に対し、MapLibreコミュニティでは次世代形式の MapLibre Tile (MLT) が議論・検討されています。

MapLibreのサイトではMapLibre Tile Specificationが公開されており、仕様の全体像を確認することができます。

GitHubではエンコーダー及びデコーダーが既に提供されており、実際にMLTを作成してみることも可能です。Imprementation Statusを確認すると、MapLibre GL JSなどに実装されているため、地図描画してみることもできます。

Introductionには、MVTをベースとしつつも、圧縮効率及びデコード性能の向上、3D対応や複雑なデータ型のサポートなど、多くの改善点が挙げられており、NEXT MVTとしてかなり期待の持てる形式になっているのではないかと思います。

私自身、仕様を余り読み込めている(理解できている)わけではないのですが、百聞は一見に如かずということで、試しに、エンコーダーでMLTを作成して性能を簡単に調べてみましたので、その結果を共有できればと思います。

エンコードツールのインストール

GitHubリポジトリのREADMEを確認すると、javaでエンコーダーが提供されているようですので、こちらをビルドして使いたいと思います。

まず、JDK21をインストールします。なお、PC環境は以下のとおりです。

  • Windows PC(Windows 11 Pro、Intel N95 1.7GHz、メモリ16GB)
  • WSL(Ubuntu)
sudo apt update
sudo apt install openjdk-21-jdk

GitHubをクローンしてビルドします。

git clone https://github.com/maplibre/maplibre-tile-spec.git
cd maplibre-tile-spec/java

./gradlew :mlt-cli:cli

ls -l mlt-cli/build/libs/
# mlt-cli/build/libs/encode.jarがあればOK

OvertureMapsからサンプルデータ取得

サンプルデータとして、OvertureMapsを使いたいと思います。

今回は、皇居周辺のデータでテストします。

様々な種別のデータで比較するため、ポリゴン(building、landuse)、ライン(road)、ポイント(place)のデータを取得します。

データ取得にはduckdbを使います。

duckdb

D load spatial;
D COPY(
  SELECT
    id,
    subtype,
    class,
    names.primary AS name,
    num_floors,
    num_floors_underground,
    geometry
  FROM read_parquet('s3://overturemaps-us-west-2/release/2026-04-15.0/theme=buildings/type=building/*', filename=true, hive_partitioning=1)
  WHERE names.primary IS NOT NULL
  AND bbox.xmin BETWEEN 139.69381 AND 139.81715
  AND bbox.ymin BETWEEN 35.66338 AND 35.70599
) TO 'building.geojson' WITH (FORMAT GDAL, DRIVER 'GeoJSON');

D COPY(
    SELECT
       id,
       subtype,
       class,
       surface,
       geometry
    FROM read_parquet('s3://overturemaps-us-west-2/release/2026-04-15.0/theme=base/type=land_use/*', filename=true, hive_partitioning=1)
    WHERE bbox.xmin BETWEEN 139.69381 AND 139.81715
	  AND bbox.ymin BETWEEN 35.66338 AND 35.70599
) TO 'landuse.geojson' WITH (FORMAT GDAL, DRIVER 'GeoJSON');

D COPY(
    SELECT
       id,
       names.primary as name,
       subtype,
       class,
       geometry
    FROM read_parquet('s3://overturemaps-us-west-2/release/2026-04-15.0/theme=transportation/type=segment/*', filename=true, hive_partitioning=1)
    WHERE subtype = 'road'
    AND bbox.xmin BETWEEN 139.69381 AND 139.81715
	  AND bbox.ymin BETWEEN 35.66338 AND 35.70599
) TO 'road.geojson' WITH (FORMAT GDAL, DRIVER 'GeoJSON');

D COPY(
    SELECT
       id,
       categories.primary as category,
       CAST(websites AS JSON) as websites,
       brand.names.primary as brand,
       theme,
       type,
       names.primary as name,
       geometry
    FROM read_parquet('s3://overturemaps-us-west-2/release/2026-04-15.0/theme=places/type=place/*', filename=true, hive_partitioning=1)
    WHERE bbox.xmin BETWEEN 139.69381 AND 139.81715
	  AND bbox.ymin BETWEEN 35.66338 AND 35.70599
	  AND confidence >= 0.99
) TO 'place.geojson' WITH (FORMAT GDAL, DRIVER 'GeoJSON');

MVT作成

エンコーダーのCLI Usageを見ると、MVTからMLTに変換する仕様となっているため、まず、MVTを作成します。

feltのtippecanoeを使ってGeoJSONからMVTに変換します。

# building
tippecanoe -z16 -Z9 -f -l building -pf -pk --simplification=5 --simplify-only-low-zooms -o building.mbtiles building.geojson

# landuse
tippecanoe -z16 -Z9 -f -l landuse -pf -pk --simplification=5 --simplify-only-low-zooms -o landuse.mbtiles landuse.geojson

# road
tippecanoe -z16 -Z9 -f -l road -pf -pk  --simplification=5 --simplify-only-low-zooms -o road.mbtiles road.geojson

# place
tippecanoe -z16 -Z9 -f -l place -pf -pk --drop-rate=0 -o place.mbtiles place.geojson

MVT→MLT変換

冒頭ビルドしたエンコーダーを使ってMVTからMLTに変換します。

# building
java -jar mlt-cli/build/libs/encode.jar \
  --mbtiles building.mbtiles \
  --dir .
# building.mlt.mbtilesが生成される

# landuse
java -jar mlt-cli/build/libs/encode.jar \
  --mbtiles landuse.mbtiles \
  --dir .
# landuse.mlt.mbtilesが生成される

# road
java -jar mlt-cli/build/libs/encode.jar \
  --mbtiles road.mbtiles \
  --dir .
# road.mlt.mbtilesが生成される

# place
java -jar mlt-cli/build/libs/encode.jar \
  --mbtiles place.mbtiles \
  --dir .
# place.mlt.mbtilesが生成される

MapLibre GL JSで可視化

MapLibre GL JSの最新バージョンでは、MLTをデコードして表示することが可能です。

実際に表示してみたのが以下のサイトになります。

mlt-dem

style.jsonを以下のように定義することでMLTでデコードしてくれます。

"building": {
  "type": "vector",
  "encoding": "mlt",
  "url": "pmtiles://pmtiles/building.mlt.pmtiles",
  "attribution": "&copy; <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors, <a href='https://docs.overturemaps.org/attribution/'>Overture Maps Foundation</a>",
  "minzoom": 9,
  "maxzoom": 16
}

なお、タイルサーバを立てなくてよいように、protomapsのツールで事前に mbtilespmtiles に変換しています。

pmtiles convert building.mbtiles building.pmtiles

データサイズ比較

MVTとMLTの、データサイズを比較してみます。

MLTをgzip圧縮

MVTはgzip圧縮されているのですが、MLTの方は未圧縮で生成されているので、土俵をそろえるため、MLTの方もgzip圧縮しておきます。

mbtiles は中身はsqlite3であるため、以下のようなpythonスクリプトでgzip圧縮します。

import sqlite3, gzip, shutil

src = "building.mlt.mbtiles"
dst = "building.mlt.gzip.mbtiles"

shutil.copyfile(src, dst)

con = sqlite3.connect(dst)
cur = con.cursor()

rows = cur.execute("""
SELECT zoom_level, tile_column, tile_row, tile_data
FROM tiles
""").fetchall()

for z, x, y, data in rows:
    if data[:2] == b"\x1f\x8b":
        continue

    gz = gzip.compress(data, compresslevel=9)

    cur.execute("""
    UPDATE tiles
    SET tile_data = ?
    WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?
    """, (gz, z, x, y))

con.commit()
con.close()

print(f"wrote {dst}")

mbtilesのサマリー出力

mbtiles内のタイルサイズを調べるため、Martinのmbtilesツールのsummaryを使います。

インストールが手間なので、Dockerコンテナを使って実施します。

# building
docker run --rm -v $(pwd):/work -w /work \
	--entrypoint "" \
	ghcr.io/maplibre/martin:latest \
	mbtiles summary building.mbtiles

MBTiles file summary for building.mbtiles
Schema: normalized
File size: 4.22MiB
Page size: 4.00KiB
Page count: 1081

 Zoom |   Count   | Smallest  |  Largest  |  Average  | Bounding Box
    9 |         1 |  135.7KiB |  135.7KiB |  135.7KiB | 139,35,140,36
   10 |         1 |  282.5KiB |  282.5KiB |  282.5KiB | 140,35,140,36
   11 |         2 |  141.0KiB |  299.2KiB |  220.1KiB | 139.6,35.6,139.9,35.7
   12 |         4 |   53.9KiB |  275.2KiB |  123.9KiB | 139.7,35.6,139.8,35.7
   13 |         8 |    4.1KiB |  208.8KiB |   66.7KiB | 139.7,35.6,139.8,35.7
   14 |        21 |    3.8KiB |  104.9KiB |   28.9KiB | 139.68,35.66,139.83,35.71
   15 |        72 |      499B |   42.7KiB |    9.6KiB | 139.69,35.66,139.82,35.71
   16 |       235 |      145B |   28.4KiB |    3.5KiB | 139.69,35.66,139.82,35.71
  all |       344 |      145B |  299.2KiB |   11.6KiB | 139.22,35.46,139.92,36.03

これを全てのMVTのmbtilesとMLTのmbtilesについて実行します。

その結果、すべてのタイルの平均サイズは以下のとおりになりました。

type MVT average size MLT average size MLT/MVT ratio(%)
building 11.6KiB 9.4KiB 81%
landuse 3.6KiB 3.0KiB 83%
road 25.4KiB 20.3KiB 80%
place 16.0KiB 12.9KiB 81%

デコード速度比較

次にMVTとMLTのデコード速度を比較してみます。

簡易的に比較するため、以下のJavaScript関数の実行所要時間で比較したいと思います。

import Pbf from "https://esm.sh/pbf";
import { VectorTile } from "https://esm.sh/@mapbox/vector-tile";

// MVTデコード
function decodeMvt(buf) {
  return new VectorTile(new Pbf(new Uint8Array(buf)));
}

import * as mlt from "https://esm.sh/@maplibre/mlt";

// MLTデコード
function decodeMlt(buf) {
  return mlt.decodeTile(new Uint8Array(buf));
}

単体のタイルで比較するため、皇居を包含する 14/14552/6451 のタイルを抽出します。

sqlite3 で以下のとおり抽出します。あらかじめgzipを解凍しておきます。

sqlite3 building.mbtiles \
"SELECT writefile('building-14-14552-6451.mvt', tile_data)
 FROM tiles
 WHERE zoom_level=14
   AND tile_column=14552
   AND tile_row=9932;"

gunzip -c building-14-14552-6451.mvt > building-14-14552-6451.raw.mvt

なお、mbtilesはTMSベースでタイルが格納されているため、XYZベースのタイル座標とはY座標が異なることに注意する必要があります。

具体的には Y(TMS) = 2**z - Y(XYZ) - 1 となります。

結果サマリー

それぞれのタイルで、300回ずつデコード関数を実行したところ、処理完了までの平均時間は以下のとおりとなりました。

type MVT average size MLT average size MLT/MVT ratio(%)
building 0.54ms 0.26ms 48%
landuse 0.40ms 0.24ms 60%
road 2.31ms 0.89ms 39%
place 1.21ms 0.17ms 14%

考察

上記のサイズ比較及びデコード速度比較から、以下のことがいえそうです。

  • サイズはいずれも80%ほどの圧縮率となる。
  • デコード速度には効果に大きな差異がみられ、place(ポイント)やroad(ライン)でより大きなパフォーマンス上の優位性が観察される。

これらの効果は、Tile Layoutに記載されているとおり、

  • MVTがレコード指向(Featureごとにデータ管理)であるのに対して、MLTが列指向であること
  • MVTが固定的なエンコーディング手法(デルタ、zigzag、dictionaryエンコーディングなど)で圧縮されるのに対して、MLTではより柔軟にかつ効果的に様々なエンコーディング手法を適用できること

に主に起因しているものと思われます。

おわりに

以上、MLTについて実施してみた試行実験を簡単に紹介させていただきました。

今回試したことや検討したこと以外でも多くの論点や特長を有しており、今後のどのように展開していくのか非常に楽しみですし、一技術者として(貢献は難しいかもしれませんが)、引き続きフォローしていきたいと思います。

全体的に踏み込み不足感はあるかと思いますが、本記事がMLT理解の一助になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?