PMTilesでタイルデータをサーバーレスで配信する（生成・配信・利用）

## はじめに

昨今のGIS界隈では**Cloud Optimized**が重要なキーワードとなっています。Cloud Optimizedの概要については下記のリンクが詳しいです。

https://qiita.com/Kanahiro/items/70b3b8b11bd26cbaf30e#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB---cloud-optimized%E3%81%A8%E3%81%84%E3%81%86%E7%94%A8%E8%AA%9E%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

このタイプのデータ形式ではラスターデータのCloud Optimized GeoTIFF(COG)が既に広く使われていて、ベクター形式は群雄割拠という様相です（2023年1月現在）。ここで変わり種として、タイルデータ（ラスター・ベクター含む）に関するCloud Optimized形式として**PMTiles**が台頭してきました。本記事ではPMTilesの概要・生成・消費を一貫して紹介します。

## PMTilesとは

https://github.com/protomaps/PMTiles

　簡単に言うと、特別なサーバー実装なしに、必要最小限のタイルデータを配信することを可能とするファイル形式です。この説明で「？」となるのは正常です。
　たとえばラスターデータのCloud Optimized形式であるCOGについて考えてみると、大きなラスターデータであっても必要最小限の部分だけを「切り出して」配信できるので、確かに便利そうです。しかしタイルデータはそもそも切られているものであって、単にタイルデータを1タイル:1ファイルとして配信すればよいだけではないかと、疑問に思う方がいるはずです。
　これはある意味で正しいです。手持ちのちょっとした航空写真データがあるとして、一度タイルに分割してサーバーに配置すればよいので、このケースでは必ずしもPMTilesは必要ないでしょう。しかしタイル数が膨大なケースを考慮する必要があります。たとえば全球タイルでズームレベル14だと、タイル数は3億を超えます。3億ファイルをサーバーへアップロードするコストは小さくないでしょう。配置するのが一度切りならば我慢できる痛みだと思いますが、そうではないことが多いはずです。
　ここで、**MBTiles**が既にこのコストを解決しているのでは、と思われるかもしれません。これも確かにそうですが、MBTilesは実体がSQLiteであり、タイルデータの配信にはサーバー実装が必要です。PMTilesは、①多数のタイルデータを1ファイルとして扱える②Cloud Optimizedの仕組みでサーバー実装を要しない、という点で優れています。ただし、③クライアント側に特別な実装を要するという点は理解しておく必要があります。

:::note info
技術的な詳細については開発者自身による下記のブログポストが詳しいです。
https://protomaps.com/blog/pmtiles-v3-hilbert-tile-ids
https://protomaps.com/blog/pmtiles-v3-layout-compression 
:::

## PMTilesを生成する

PMTilesの生成には、以下のいずれかを利用できます。

https://github.com/protomaps/go-pmtiles
https://github.com/felt/tippecanoe

　前者、protomaps/go-pmtilesがPMTilesの開発者によるリポジトリです。また、tippecanoeがv2.17.0でPMTilesの出力に対応したことは特筆すべき点です。手持ちのベクターデータをPMTilesにしたいならtippecanoeがシンプルですし、ラスタデータもしくは手持ちのMBTilesをPMTilesにするならgo-pmtilesを用いるのがよいでしょう。本記事ではgo-pmtilesを用いてPMTilesを生成してみます。

### 検証用データ

go-pmtilesでPMTilesを生成するにはMBTilesが必要です。今回は**MapTiler Data**の評価版データを利用します。

https://data.maptiler.com/downloads/planet/

非商用・個人プロジェクトなら無料で試用することができます。50GBくらいあります。ファイル名は`maptiler-osm-2017-07-03-v3.6.1-planet.mbtiles`となりました。

### go-pmtilesでPMTilesを生成する

:::note info
RAMが16GB載っている`c5.2xlarge`で実行しました。`c5.xlarge`だと途中で落ちました。
:::

go-pmtilesは[ビルド済みバイナリが配布されている](https://github.com/protomaps/go-pmtiles/releases)ので、コンパイル等は不要です。実行環境にあったファイルをダウンロード・展開しておきましょう。

```sh
./pmtiles convert maptiler-osm-2017-07-03-v3.6.1-planet.mbtiles output.pmtiles
```

```
2023/01/05 14:09:02 convert.go:366: Root dir bytes:  9664
2023/01/05 14:09:02 convert.go:367: Leaves dir bytes:  59911110
2023/01/05 14:09:02 convert.go:368: Num leaf dirs:  2024
2023/01/05 14:09:02 convert.go:369: Total dir bytes:  59920774
2023/01/05 14:09:02 convert.go:370: Average leaf dir bytes:  29600
2023/01/05 14:09:02 convert.go:371: Average bytes per addressed tile: 0.48
2023/01/05 14:14:03 convert.go:344: Finished in  2h14m6.206522602s

real    134m7.881s
user    28m18.093s
sys     8m56.547s
```

変換前のMBTilesは54GBでしたが、PMTilesにすると35GBに**減少しています**。

```
ls -l
-rw-rw-r-- 1 ubuntu ubuntu 54776152064 Aug 31  2017 maptiler-osm-2017-07-03-v3.6.1-planet.mbtiles
-rw-rw-r-- 1 ubuntu ubuntu 35728910401 Jan  5 14:14 output.pmtiles
```

これはおそらく、repeated tiles（繰り返し登場する同じ内容のタイル）のデータ取り扱いの効率化によるものと考えられます。

以下、生成したPMTilesは`https://<sampleurl>/output.pmtiles`として配信されているものとします。なお、PMTilesを適切に配信するには、HTTP Range-Requestに対応したウェブサーバーである必要があります。現代のウェブサーバーなら普通に対応している機能です。今回の検証ではS3にアップロードしてみました。

### おまけ：tippecanoeでPMTilesを生成する

v2.17.0以降のtippecanoeでもPMTilesを生成出来ます。

```sh
tippecanoe --version
# tippecanoe v2.17.0

# 単に、拡張子をpmtilesとすればよい
tippecanoe -o output.pmtiles input.geojsonl
# MBTilesの生成処理が走り、完了後にPMTilesへの変換処理が走る。前者に比べて後者の処理時間は短い
```

## PMTilesのデータをブラウザで表示する

PMTilesはクライアント側に特殊な実装を要求しますが、`protomaps/PMTiles`以下で、その実装が公開されています。今回は`MapLibre GL JS`で表示します。

https://github.com/protomaps/PMTiles/blob/main/js/README.md

```sh
npm install pmtiles
npm install maplibre-gl
```

```typescript
import { Map } from 'maplibre-gl';
import * as pmtiles from 'pmtiles';

const protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles",protocol.tile);

const map = new Map({
    container: 'map',
    style: {
        version: 8,
        sources:{
            pmtiles: {
                type:'vector',
                url: 'pmtiles://https://<sampleurl>/output.pmtile',
                attribution: '© MapTiler © OpenStreetMap contributors'
            }
        },
        layers:[
            {
                id: 'water',
                source: 'pmtiles',
                'source-layer': 'water',
                type: 'fill',
                paint: {
                    'fill-color': '#0000ff',
                },
            },
            {
                id: 'landcover',
                source: 'pmtiles',
                'source-layer': 'landcover',
                type: 'fill',
                paint: {
                    'fill-color': '#00ff00',
                },
            },
            {
                id: 'boundary',
                source: 'pmtiles',
                'source-layer': 'boundary',
                type: 'line',
                paint: {
                    'line-color': '#ff0000',
                },
            },
            {
                id: 'place',
                source: 'pmtiles',
                'source-layer': 'place',
                type: 'circle',
                paint: {
                    'circle-color': '#ff9900',
                },
            },
        ],
    }
});
```

![Screen Shot 2023-01-06 at 9.19.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73197/7c12804f-e5ba-2b21-e636-0330492b8a19.png)

![Screen Shot 2023-01-06 at 9.19.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73197/2c773198-138a-51c1-c587-66be92b2ac15.png)

たったこれだけのコードで、PMTilesのクライアント実装が完了しました。速度も一般のタイルデータの場合と遜色なく、高速に動作します。

### metadataの読み取り

PMTilesは、MBTilesに含まれるmetadataも内包しています。下記のように読み取ることができ、非常に便利です。

```typescript
const _pmtiles = new pmtiles.PMTiles(
    'https://<sampleurl>/output.pmtiles',
);
_pmtiles.getMetadata().then((res) => console.log(res));
/**
{
    "attribution": "<a href=\"http://www.openmaptiles.org/\" target=\"_blank\">&copy; OpenMapTiles</a> <a href=\"http://www.openstreetmap.org/about/\" target=\"_blank\">&copy; OpenStreetMap contributors</a>",
    "description": "A tileset showcasing all layers in OpenMapTiles. http://openmaptiles.org",
    "format": "pbf",
    "id": "openmaptiles",
    "maskLevel": "8",
    "maxzoom": "14",
    "minzoom": "0",
    "mtime": "1499626373833",
    "name": "OpenMapTiles",
    "pixel_scale": "256",
    "planettime": "1499040000000",
    "vector_layers": [
        {
            "description": "",
            "fields": {
                "class": "String"
            },
            "id": "water",
            "maxzoom": 14,
            "minzoom": 0
        },
// 以下略
    ],
    "version": "3.6.1"
}
*/
```

## 終わりに

　結構前からPMTilesは追っているのですが、パフォーマンスが向上している（たぶん）など、成熟していきている感があります。とても便利なフォーマットなのでどんどん取り入れていきたいところです。タイル系の形式としてデファクトと考えてよいでしょう。あとはQGISがネイティブ対応することがあれば、とてもうれしいですね。