MBTiles でタイル地図のオフライン利用

## タイル地図

国土地理院の[地理院タイル](https://maps.gsi.go.jp/development/siyou.html)をはじめとするタイル地図の登場で、背景図を簡易に、かつ快適に利用することができるようになりました。また、すべてのタイル地図に当てはまるわけではありませんが、空中写真も含め無償で利用できるものも多く、助かります。

QGIS 2.x では [TileLayer Plugin](https://github.com/minorua/TileLayerPlugin) を利用するケースが多かったですが、 QGIS 3.x （正確には 2.18 も対応）からは標準で XYZ Tiles が追加できるようになりました。このように便利なタイル地図ですが、基本的に HTTP(S) 上にある画像データであるため、利用するときは（内部で）画像をダウンロードする必要があります。しかし、 GIS で個人情報等も一緒に扱うなどの理由でインターネットに接続しない環境で GIS を使いたいという要望も耳にします。

タイル地図をオフラインで使うことができるか。という問題ですが、基本的に一定のルールにしたがって配置された画像の集まりなので、ローカルに保存したタイル画像でも仕組み上は問題なさそうです（実際 TileLayer Plugin はローカルパスに対応）。<del>しかし、 QGIS 標準の XYZ Tiles は現時点でローカルのパスには対応していないようです。</del> <ins>QGIS 3.4 ではローカルパスに対応したようです。</ins>[^LocalXYZ]

[^LocalXYZ]: [[Qgis-user] QGIS Local XYZ Tiles](https://lists.osgeo.org/pipermail/qgis-user/2018-September/041118.html) / [コミットログ fixes #16587](https://github.com/qgis/QGIS/commit/87aa9320415ba21e0766d4d132ce190a56538a0a#diff-2093443a79b7cfa3a61aafa07cbadb6e)

またタイル地図は 256px 四方の画像の集まりなので非常にファイル数が多くなります。このため一旦タイル地図をダウンロードできても、バックアップや他 PC へファイルのコピーを行う場合には非常に時間がかかります。

## そもそもダウンロードしていいの？

っと、その前に地理院タイルをローカルにダウンロードして利用してよいのでしょうか。

地理院タイルには大きくわけて基本測量成果を使用したタイルと、基本測量成果には当たらないタイルがあります。[地理院タイル一覧](https://maps.gsi.go.jp/development/ichiran.html)にわかりやすく分類されていますが、空中写真や衛星写真は基本測量の成果には当たりません。また基本測量の成果に当たるものは原則[測量法29条30条](http://www.gsi.go.jp/LAW/2930-index.html)に従い国土地理院の長の承認を得る必要がありますが、条件を満たせば出典明示などで利用することができます。たとえば社内のイントラネットで利用する場合は30台を上限に複製できるとされています（[よくある質問と回答](http://www.gsi.go.jp/LAW/2930-qa.html#03)）。ただし印刷し配布といった使用を行う場合は別問題なので注意が必要です。

また、基本測量成果に当たらない空中写真等は[国土地理院コンテンツ利用規約](http://www.gsi.go.jp/kikakuchousei/kikakuchousei40182.html)に従い自由に複製することができます。

ただし、困ったら申請を行っておくほうが安心して利用できるかと思います。

ダウンロードに関しては国土地理院から [qdltc](https://github.com/gsi-cyberjapan/qdltc) というツール（スクリプト）が公開されています。 qdltc は目録ファイルをもとに全タイルを並列ダウンロードするツールです。全ファイルのダウンロードは1週間くらいかかるそうです。ただし、2回目以降の更新分ダウンロードは MD5 ダイジェストを確認することにより短時間ですむようになっているそうです。

たとえば `std` の目録ファイルは以下の場所にあります。 `std` の場合、目録ファイルだけで1.1 GB ほどあります。

* https&#58;//cyberjapandata.gsi.go.jp/xyz/std/mokuroku.csv.gz

目録ファイルはパス、最終更新時刻、ファイルサイズ、 MD5 ダイジェストをまとめた CSV ファイルを gzip で圧縮したものです。

```shell-session
$ gzip -dc mokuroku.csv.gz | head
0/0/0.png,1457603626,77992,7186204bdc21218bf6d9dd0a39b17ef8
1/0/0.png,1457603627,75099,8abde99545d93135816895dd90871856
1/0/1.png,1457607266,49114,b1c45de27b2532a2c11710ad22851675
1/1/0.png,1457607264,76255,0d99409cff6edaad7a722a262a133180
1/1/1.png,1457603624,46597,15ca0df415ce6a3274edf96f65ac65f5
10/861/365.png,1450706972,222,738667a1b2ac6a61a03addc227320bb8
10/861/366.png,1450706950,222,edd701bef084ea879f7845e5cf140266
10/861/367.png,1450706951,222,c43784cad649d3664a2aaf68f035b6fa
10/861/368.png,1450706949,222,beca812cd632578f01f066cda486c8e6
10/861/369.png,1450706970,222,8ea3c54f6da6e594e5f90a2b3e81bdbb
```

私は、（たとえば）東京23区はズームレベル18まで、横浜大宮千葉あたりまではズームレベル16まで、関東周辺はズームレベル13まで、その他地域はズームレベル9までみたいなことを、手作業で地域指定してダウンロード範囲を決めるスクリプトを使っています。

もちろん、夜間に、適度な待ち時間をはさみながら。サーバ資源大事大事。

国土地理院の目録の活用は[地理院タイル目録を用いた地理院タイルの効率的な同期方法](http://www.gsi.go.jp/common/000106650.pdf)も参考になります。

## MBTiles

[MBTiles](https://github.com/mapbox/mbtiles-spec) は SQLite を利用したラスタ形式のひとつで、タイル地図をひとつのファイルにまとめることができます。 MBTiles に対応しているツールはいくつかありますが、今回は Mapbox 社からでている [mb-util](https://github.com/mapbox/mbutil) を使うことにします。

mb-util は `easy_install` などでインストールしてもよいのですが、リポジトリをクローンすれば Python スクリプトとしてそのまま使えます。

たとえば ort にダウンロードしたタイルが `z/x/y` の形式のパスで格納されているとします。

```shell-session
$ tree ort | head -n 20
ort
|-- 10
|   |-- 900
|   |   |-- 396.jpg
|   |   |-- 397.jpg
|   |   |-- 398.jpg
|   |   |-- 399.jpg
|   |   |-- 400.jpg
|   |   |-- 401.jpg
|   |   |-- 402.jpg
|   |   |-- 403.jpg
|   |   |-- 404.jpg
|   |   |-- 405.jpg
|   |   |-- 406.jpg
|   |   `-- 407.jpg
|   |-- 901
|   |   |-- 396.jpg
|   |   |-- 397.jpg
|   |   |-- 398.jpg
|   |   |-- 399.jpg

$ du -hs ort
1.3G	ort
```

手元にあったものは 1.3 GB くらいのデータですね。これを `mb-util` で変換します。

```shell-session
$ mb-util --silent --image_format=jpg ort ort.mbtiles
53200 tiles inserted (2020 tiles/sec)

$ ls -lh ort.mbtiles 
-rw-r--r--. 1 tohka tohka 1.2G 2018-12-31 16:31 ort.mbtiles
```

今回のデータだと、変換は1分程度で終わりました。 `gdalinfo` でラスタ情報を確認してみます。

```shell-session
$ gdalinfo ort.mbtiles 
Driver: MBTiles/MBTiles
Files: ort.mbtiles
Size is 131072, 131072
Coordinate System is:
PROJCS["WGS 84 / Pseudo-Mercator",
    GEOGCS["WGS 84",
        DATUM["WGS_1984",
            SPHEROID["WGS 84",6378137,298.257223563,
                AUTHORITY["EPSG","7030"]],
            AUTHORITY["EPSG","6326"]],
        PRIMEM["Greenwich",0,
            AUTHORITY["EPSG","8901"]],
        UNIT["degree",0.0174532925199433,
            AUTHORITY["EPSG","9122"]],
        AUTHORITY["EPSG","4326"]],
    PROJECTION["Mercator_1SP"],
    PARAMETER["central_meridian",0],
    PARAMETER["scale_factor",1],
    PARAMETER["false_easting",0],
    PARAMETER["false_northing",0],
    UNIT["metre",1,
        AUTHORITY["EPSG","9001"]],
    AXIS["X",EAST],
    AXIS["Y",NORTH],
    EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext  +no_defs"],
    AUTHORITY["EPSG","3857"]]
Origin = (15497753.906250000000000,4383203.125000000000000)
Pixel Size = (1.194328069686890,-1.194328069686890)
Image Structure Metadata:
  INTERLEAVE=PIXEL
Corner Coordinates:
Upper Left  (15497753.906, 4383203.125) (139d13' 7.29"E, 36d35'52.35"N)
Lower Left  (15497753.906, 4226660.156) (139d13' 7.29"E, 35d27'38.37"N)
Upper Right (15654296.875, 4383203.125) (140d37'29.79"E, 36d35'52.35"N)
Lower Right (15654296.875, 4226660.156) (140d37'29.79"E, 35d27'38.37"N)
Center      (15576025.391, 4304931.641) (139d55'18.54"E, 36d 1'52.75"N)
Band 1 Block=256x256 Type=Byte, ColorInterp=Red
  Overviews: 65536x65536, 32768x32768, 16384x16384, 8192x8192, 4096x4096, 2048x2048, 1024x1024, 512x512, 256x256
Band 2 Block=256x256 Type=Byte, ColorInterp=Green
  Overviews: 65536x65536, 32768x32768, 16384x16384, 8192x8192, 4096x4096, 2048x2048, 1024x1024, 512x512, 256x256
Band 3 Block=256x256 Type=Byte, ColorInterp=Blue
  Overviews: 65536x65536, 32768x32768, 16384x16384, 8192x8192, 4096x4096, 2048x2048, 1024x1024, 512x512, 256x256
```

## うまく表示されない？

このファイルを QGIS で追加すると表示はされます。しかしズームレベルが小さなタイルほど広域に取得したにも関わらず、表示されるのは最大ズームレベルで取得した限られた範囲のみになります。

どうも（ QGIS 内でラスタの読み込みに使われる） GDAL では範囲情報が存在しない場合、最大ズームレベルの範囲をもって、ラスタ全体の範囲と計算しているようで、その結果、範囲外の部分に関しては表示してくれないようです。

* [gdal/frmts/mbtiles/mbtilesdataset.cpp](https://github.com/OSGeo/gdal/blob/master/gdal/frmts/mbtiles/mbtilesdataset.cpp#L2180-L2187)

<img width="600" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/215290/647710eb-5c3c-58af-8f8b-322708a93fd1.png">
出典：国土地理院オルソ画像、 Landsat8 画像（GSI, TSIC, GEO Grid/AIST）、 
 Landsat8 画像（courtesy of the U.S. Geological Survey）、 海底地形（GEBCO）

範囲情報を付加した MBTiles と、付加していないため部分的にしか表示されていない MBTiles 。

そこで範囲等の情報を追加した MBTiles を作成します。 `mb-util` の場合範囲等のメタ情報は `metadata.json` という名称でファイルを（今回のディレクトリ配置だと） `ort` に追加します。（ `mb-util` 以外のツールだとオプションなど別の方法が用意されている場合があります。また最悪、作成した MBTiles ファイルに対し SQLite として `metadata` テーブルにレコードを追加するという方法もあります。）

```shell-session
$ vim ort/metadata.json

$ tree ort | head -n 10
ort
|-- 10
|   |-- 900
|   |   |-- 396.jpg
|   |   |-- 397.jpg
|   |   |-- 398.jpg
|   |   |-- 399.jpg
|   |   |-- 400.jpg
|   |   |-- 401.jpg
|   |   |-- 402.jpg

$ tree ort | tail -n 10
|   `-- 457
|       |-- 198.jpg
|       |-- 199.jpg
|       |-- 200.jpg
|       |-- 201.jpg
|       |-- 202.jpg
|       `-- 203.jpg
`-- metadata.json

```

今回用意した `metadata.json` の中身はこんなかんじ。 `bounds` に範囲を指定します。わからない場合や、 `0/0/0` などタイル地図で表現できる全域の場合は `-180.0,-85.051129,180.0,85.051129` を指定すればよいでしょう。

```json
{
    "name": "MBTiles demo",
    "type": "baselayer",
    "description": "demo",
    "version": "1",
    "format": "jpg",
    "bounds": "136.40625,34.307143856288036,142.03125,37.718590325588146",
    "minzoom": "8",
    "maxzoom": "17"
}
```

指定できる項目は [MBTiles Specification](https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md#content) に載っています。また、 [mb-util のテストデータ](https://github.com/mapbox/mbutil/tree/master/test/data/tiles/zyx)も参考になるかと思います。

この状態で（一旦古いファイルを消して）あらためて `mb-util` を実行すると `metadata.json` が自動的に読み込まれて範囲等の情報が設定されます。

```shell-session
$ rm ort.mbtils

$ mb-util --silent --image_format=jpg ort ort.mbtiles
53200 tiles inserted (1900 tiles/sec)
```

もういっかい `gdalinfo` にかけてみます。

```shell-session
$ gdalinfo ort.mbtiles 
Driver: MBTiles/MBTiles
Files: ort.mbtiles
Size is 524288, 393216
Coordinate System is:
PROJCS["WGS 84 / Pseudo-Mercator",
    GEOGCS["WGS 84",
        DATUM["WGS_1984",
            SPHEROID["WGS 84",6378137,298.257223563,
                AUTHORITY["EPSG","7030"]],
            AUTHORITY["EPSG","6326"]],
        PRIMEM["Greenwich",0,
            AUTHORITY["EPSG","8901"]],
        UNIT["degree",0.0174532925199433,
            AUTHORITY["EPSG","9122"]],
        AUTHORITY["EPSG","4326"]],
    PROJECTION["Mercator_1SP"],
    PARAMETER["central_meridian",0],
    PARAMETER["scale_factor",1],
    PARAMETER["false_easting",0],
    PARAMETER["false_northing",0],
    UNIT["metre",1,
        AUTHORITY["EPSG","9001"]],
    AXIS["X",EAST],
    AXIS["Y",NORTH],
    EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext  +no_defs"],
    AUTHORITY["EPSG","3857"]]
Origin = (15184667.968750000000000,4539746.093750000000000)
Pixel Size = (1.194328069686890,-1.194328069686890)
Metadata:
  name=MBTiles demo
  format=jpg
  bounds=136.40625,34.307143856288036,142.03125,37.718590325588146
  minzoom=8
  version=1
  maxzoom=17
  type=baselayer
  description=demo
Image Structure Metadata:
  INTERLEAVE=PIXEL
Corner Coordinates:
Upper Left  (15184667.969, 4539746.094) (136d24'22.30"E, 37d43' 6.88"N)
Lower Left  (15184667.969, 4070117.188) (136d24'22.30"E, 34d18'25.67"N)
Upper Right (15810839.844, 4539746.094) (142d 1'52.29"E, 37d43' 6.88"N)
Lower Right (15810839.844, 4070117.188) (142d 1'52.29"E, 34d18'25.67"N)
Center      (15497753.906, 4304931.641) (139d13' 7.29"E, 36d 1'52.75"N)
Band 1 Block=256x256 Type=Byte, ColorInterp=Red
  Overviews: 262144x196608, 131072x98304, 65536x49152, 32768x24576, 16384x12288, 8192x6144, 4096x3072, 2048x1536, 1024x768
Band 2 Block=256x256 Type=Byte, ColorInterp=Green
  Overviews: 262144x196608, 131072x98304, 65536x49152, 32768x24576, 16384x12288, 8192x6144, 4096x3072, 2048x1536, 1024x768
Band 3 Block=256x256 Type=Byte, ColorInterp=Blue
  Overviews: 262144x196608, 131072x98304, 65536x49152, 32768x24576, 16384x12288, 8192x6144, 4096x3072, 2048x1536, 1024x768
```

また SQLite で `metadata` テーブルをみてみます。

```shell-session
$ sqlite3 ort.mbtiles 'SELECT * FROM metadata;'
name|MBTiles demo
format|jpg
bounds|136.40625,34.307143856288036,142.03125,37.718590325588146
minzoom|8
version|1
maxzoom|17
type|baselayer
description|demo
```