Web GIS のズーム・レベルとタイル・キャッシュの設計

## 1. ズーム・レベル

ズーム・レベルの設計の良し悪しは、Web GIS の使い心地を大きく左右します。

ズーム・レベルの設計が良くないと、目的の地物を適切な大きさで表示することが出来ません。ひどいときには、ズームインやズームアウトをした拍子に地物を見失ったりもします。

ストレス無く Web GIS を使えるようにするためには、ユーザ・フレンドリーなズーム・レベルを設計する必要があります。特に難しいことはありませんが、いくつか要点があります。

### 1-1. 上限と下限を適切に決める

最初にズーム・レベルの上限と下限を決めます。

上限および下限としてどのような値が適切かは表示する地図によってさまざまです。QGIS 等の Desktop GIS で表示を確認しながら、適切な上限と下限を決めてください。

:::note
どっちが上限でどっちが下限？

- `縮小して広い範囲を表示` = `縮尺の分母が大きい` = `小縮尺`
- `拡大して狭い範囲を表示` = `縮尺の分母が小さい` = `大縮尺`

というのが一般的な用語です。従って、小縮尺側を下限、大縮尺側を上限とします。
:::

とりあえずは、下限だけを決めても構いません。すなわち、これ以上表示範囲を広くすると私の地図が小さくなりすぎて意味が無くなる、というあたりの縮尺を下限として決めます。

上限の方は、たいてい、やっていくうちに自ずと決ります。

### 1-2. 比率を 1:√2 にする

経験上、各ズーム・レベルの間の比率を 1:2 ではなく、1:√2 にするのがお奨めです。

つまり、ズーム・ボタンを一回押すと縮尺が倍になる、というのではなく、一回で 1.41421356 倍になり、二回で倍になる、という具合にします。

1:2 の比率では間隔が広すぎて気持ちが落ち着かないけれど、1:√2 の比率にすると安心してズームイン、ズームアウトの操作ができます ... 少なくとも私はそうです。

### 1-3. Excel でズーム・レベル表を作成する

下のような表を Excel で作成します。

| Level | 基準縮尺（の分母） | UI 縮尺（の分母） | 画素値(m/d) |
|-----:|--------:|-------:|---------:|
|0|1024000|1000000|264.583333333333|
|1|724077.343935025|750000|198.4375|
|2|512000|500000|132.291666666667|
|3|362038.671967512|350000|92.6041666666667|
|4|256000|250000|66.1458333333333|
|5|181019.335983756|180000|47.625|
|6|128000|125000|33.0729166666667|
|7|90509.667991878|90000|23.8125|
|8|64000|65000|17.1979166666667|
|9|45254.833995939|45000|11.90625|
|10|32000|30000|7.9375|
|11|22627.4169979695|20000|5.29166666666667|
|12|16000|15000|3.96875|
|13|11313.7084989847|10000|2.64583333333333|
|14|7999.99999999999|8000|2.11666666666667|
|15|5656.85424949237|5500|1.45520833333333|
|16|3999.99999999999|4000|1.05833333333333|
|17|2828.42712474619|3000|0.79375|
|18|2000|2000|0.529166666666667|
|19|1414.21356237309|1500|0.396875|
|20|999.999999999998|1000|0.264583333333333|
|21|707.106781186546|700|0.185208333333333|
|22|499.999999999999|500|0.132291666666667|
|23|353.553390593273|350|0.0926041666666667|
|24|250|250|0.0661458333333333|

- 上記では、レベル 0 から 24 まで、25 段階のズームを定義
- 基準縮尺（の分母）の列は自動計算
    - `=B1/sqrt(2)` のような式を埋め込んで、比率を 1:√2 で変化させる
    - 1024000 のような **計算上** 切りの良い値から始めるとよい
- UI 縮尺（の分母）の列は手入力
    - 基準縮尺の値を参照しながら、**人間にとって** 切りの良い数値にする
- 画素値の列は自動計算
    - 「画素値」は `meter/dot` すなわち 1 画素あたりのメーター数を示す値
    - `画素値 = 縮尺の分母 / 3779.527559` で計算
    - この数値を見れば、ズームの上限をどこに置くかを判断しやすい
    - この値は、後でタイル・キャッシュの設計でも用いる

:::note warn
「画素値」という用語は一般に流通している言葉ではありません。「1 画素あたりのメーター数」を示す適当な用語が見当たらないので、この文書ではそれを「画素値」と呼ぶことにします。
:::

:::note
画素値の計算の根拠

- 1 画素の大きさは、通常のモニターで、1/96 inch（96 DPI）
- 1 inch = 0.0254 m
- 従って、1 m あたりの画素数は、96 / 0.0254 = 3779.527559
- 縮尺の分母の値 = 地図上の 1m の実際の m 数
- 従って、画素値 = 縮尺の分母 / 3779.527559

:::

要するに、1:√2 で変化する基準縮尺を参考にしながら、人間が読み取りやすい縮尺値をおおむね 1:√2 で変化させよう、ということです。基準縮尺は目安に過ぎないと考えて構いません。

また、地図によっては、部分的に縮尺の間隔を広げたり狭めたりする方が使い勝手が良いかも知れません。

### 1-4. ビューワのズーム・レベルを設定する

`qwc-services` / `QWC2` の場合は、`themesConfig.json` の `defaultScales` またはテーマごとの `scales` に上記の UI 縮尺のリストを設定します。

```json:themesConfig.json
{
    "defaultScales": [
        1000000,
        750000,
        500000,
        350000,
        250000,
        180000,
        125000,
        90000,
        65000,
        45000,
        30000,
        20000,
        15000,
        10000,
        8000,
        5500,
        4000,
        3000,
        2000,
        1500,
        1000,
        700,
        500,
        350,
        250
    ],
    ...
```

## 2. タイル・キャッシュの設計

タイル・キャッシュは、ビューワのズーム・レベルに合せた形で設計するのが合理的です。

タイル・キャッシュはビューワから要求された縮尺に最も近い縮尺のタイル画像を返します。縮尺に違いがある場合は、ビューワ側での拡大・縮小が必要になりますので、出来れば要求通りの縮尺で画像を返したいものです。

ただし、タイル・キャッシュはディスク・リソースを大量に消費しますので、節約できるところは節約する工夫が必要です。

なお、以下では `MapCache` を例として話を進めます。と言うのは、筆者が `MapCache` しか使ったことが無いからです。しかし、基本的な考え方は他の地図画像キャッシュ・アプリでも同じであろうと思いますので、参考にしてください。

要点は、

- ベクタ・レイヤ用とラスタ・レイヤ用で異なるグリッドを使う
- ラスタ・レイヤ用のグリッドでは、足切りと間引きを行う

です。

### 2-1. ベクタ用グリッドは画素値のリストに従う

下記が、筆者が使用しているベクタ・レイヤ用のグリッドです。

```xml:mapcache.xml
    <grid name="GV">
        <metadata>
            <title>Grid for Vector Layers</title>
        </metadata>
        <srs>EPSG:3857</srs> <!--  WGS 84 / Pseudo-Mercator -->
        <units>m</units>
        <extent>
            15002000.0
             4156000.0
            15060000.0
             4207000.0
        </extent>
        <resolutions>
            <!-- m/dot      level scale(縮尺) -->
            264.5833333  <!-- 0 : 1/1,00,000 -->
            198.4375     <!-- 1 : 1/750,000 -->
            132.2916667  <!-- 2 : 1/500,000 -->
            92.60416667  <!-- 3 : 1/350,000 -->
            66.14583333  <!-- 4 : 1/250,000 -->
            47.625       <!-- 5 : 1/180,000 -->
            33.07291667  <!-- 6 : 1/125,000 -->
            23.8125      <!-- 7 : 1/90,000 -->
            17.19791667  <!-- 8 : 1/65,000 -->
            11.90625     <!-- 9 : 1/45,000 -->
            7.9375       <!-- 10: 1/30,000 -->
            5.291666667  <!-- 11: 1/20,000 -->
            3.96875      <!-- 12: 1/15,000 -->
            2.645833333  <!-- 13: 1/10,000 -->
            2.116666667  <!-- 14: 1/8,000 -->
            1.455208333  <!-- 15: 1/5,500 -->
            1.058333333  <!-- 16: 1/4,000 -->
            0.79375      <!-- 17: 1/3,000 -->
            0.529166667  <!-- 18: 1/2,000 -->
            0.396875     <!-- 19: 1/1,500 -->
            0.264583333  <!-- 20: 1/1,000 -->
            0.185208333  <!-- 21: 1/700 -->
            0.132291667  <!-- 22: 1/500 -->
            0.092604167  <!-- 23: 1/350 -->
        </resolutions>
        <size>256 256</size>
        <read-only>false</read-only>
    </grid>
```

- `extent` （対象領域）については後述
- `resolutions` のリストに、Excel ズーム・レベル表の画素値の値を設定
- ただし、Level 24 は省略

:::note warn
Level が 1 つ上がるとタイル数は 2 倍

縮尺が √2 倍になると、タイル数は √2 x √2 で 2 倍になります。タイル数は幾何級数的に増えますので、ある程度の所で止めないと、あっと言う間にディスクがいっぱいになります。
:::

:::note
逆に、小縮尺側はかなり贅沢にしてもディスクをほとんど消費しません。
:::

### 2-2. ラスタ用グリッドでは足切りと間引きをする

下記が、筆者が使用しているラスタ・レイヤ用のグリッドです。

```xml:mapcache.xml
    <grid name="GR">
        <metadata>
            <title>Grid for Raster Layers</title>
        </metadata>
        <srs>EPSG:3857</srs> <!--  WGS 84 / Pseudo-Mercator -->
        <units>m</units>
        <extent>
            15002000.0
            4156000.0
            15060000.0
            4207000.0
        </extent>
        <resolutions>
            <!-- m/dot      level scale(縮尺) -->
            264.5833333  <!-- 0 : 1/1,00,000 -->
            198.4375     <!-- 1 : 1/750,000 -->
            132.2916667  <!-- 2 : 1/500,000 -->
            92.60416667  <!-- 3 : 1/350,000 -->
            66.14583333  <!-- 4 : 1/250,000 -->
            47.625       <!-- 5 : 1/180,000 -->
            33.07291667  <!-- 6 : 1/125,000 -->
            23.8125      <!-- 7 : 1/90,000 -->
            17.19791667  <!-- 8 : 1/65,000 -->
            11.90625     <!-- 9 : 1/45,000 -->
            7.9375       <!-- 10: 1/30,000 -->
            5.291666667  <!-- 11: 1/20,000 -->
            3.96875      <!-- 12: 1/15,000 -->
            <!-- 2.645833333  XX: 1/10,000 -->
            2.116666667  <!-- 13: 1/8,000 -->
            <!-- 1.455208333  XX: 1/5,500 -->
            1.058333333  <!-- 14: 1/4,000 -->
            <!-- 0.79375      XX: 1/3,000 -->
            0.529166667  <!-- 15: 1/2,000 -->
        </resolutions>
        <size>256 256</size>
        <read-only>false</read-only>
    </grid>
```

- `resolutions` のリストに、Excel ズーム・レベル表の画素値の値を設定
- ただし、画素値 0.529166667 のところで終了し、残りは割愛する
- また、大縮尺でいくつかレベルを間引きする

#### 2-2-1. 足切りの根拠

筆者が扱うラスタ・レイヤのソースで最も解像度の高いデータは、兵庫県が提供している高精度3次元地理空間データ、すなわち、50cm メッシュの DEM や DSM または CS 立体図です。つまり、**0.5 meter/dot** のデータです。

このデータについて、0.5 meter/dot より縮尺の大きなタイル・キャッシュを用意することは可能ですが、あまり意味がありません。キャッシュされるのは拡大された画像に過ぎず、ビューワでも細かい四角形が目立つぼやけた画像になります。

ソース・データの縮尺で足切りをすろと、それを超える縮尺の地図を表示するためにはビューワ側で拡大処理をする必要が生じるのですが、昨今の PC であれば、そのための CPU 負荷は体感できるほど大きくはありません。

それよりも、**Level が 1 つ上がると 2 倍** になるタイル数を節約できるメリットが非常に大きいことが、この足切りを正当化する最大の理由になります。

#### 2-2-2. 間引きの根拠

ベクタ・データに比べると、ラスタ・データの拡大・縮小に伴う画像の荒れは、特に大縮尺側において、あまり目立たない傾向があります。

ここでも、ディスク領域の節約のために、ラスタ・データについては大縮尺側でいくつかのグリッド・レベルを間引くことにします。

:::note warn
ベクタ・レイヤでは、足切りと間引きを避けること

ベクタ・レイヤの画像キャッシュは、ベクタをラスタ化した結果の画像ですので、拡大・縮小すると、ギザギザになったり、線が途切れたりと、何かと荒れが目立ちます。従って、可能な限り、ビューワが要求する縮尺のタイルを返すようにするべきです。

また、ベクタ・レイヤでは、足切りや間引きによるディスク容量節約効果がラスタ・レイヤに比べると非常に小さなものになります。理由は:

- 色数が少ないため圧縮が良く効いて、1 枚のタイルのサイズが小さくなる
    - ラスタ・レイヤの 256 x 256 のタイルは、数十KB
    - ベクタ・レイヤのそれは、約十分の一（数KB）
- 離散的なデータであったり、空白（透明）部分があったりするので、超軽量な `NoData` タイルが多くなる

以上の理由から、ベクタ・レイヤでは、足切りと間引きはなるべく避ける方が賢明です。
:::

### 2-3. グリッドの領域を設定する

```xml:mapcache.xml
        <extent>
            15002000.0
            4156000.0
            15060000.0
            4207000.0
        </extent>
```

上記は、`mapcache.xml` でグリッドの対象領域を設定している部分です。順に、`min_x`（西）、`min_y`（南）、`max_x`（東）、そして、`max_y`（北）の `ESPG:3857` による座標値で指定しています。

この領域は、将来を見越して、思い切って広めに取っておくことをお奨めします。

`MapCache` では、キャッシュするレイヤごとに、グリッド内の使用領域を限定することが出来ますので、その機能を利用します。

```xml:mapcache.xml
    <tileset name="tanada">
        <source>tanada</source>
        <cache>disk</cache>
        <grid restricted_extent="15015288 4180523 15015951 4181927"
              tolerance="0" minzoom="10">GV</grid>
        <format>PNG</format>
        <metatile>5 5</metatile>
        <metabuffer>10</metabuffer>
    </tileset>
```

`minzoom` および `maxzoom` による使用ズーム・レベルの限定も可能です。

このようにすれば、将来、レイヤの領域を変更したり、別のレイヤを追加したりするときに、既存のグリッドを使い続けることが可能になります。グリッドの定義から修正しなければならないとなると、それまでにシーディングされてきたキャッシュ・データを全て破棄しなければならなくなりますので、グリッドの定義はなるべく変更する必要がないようにしておきたいのです。

なお、実際にディスク領域の消費量を決定するのは、レイヤごとの限定された領域とズーム・レベルです。グリッド自体が定義する領域とズーム・レベルは、どんなに贅沢な仕様になっていても、それだけではディスク領域を全く消費しません。

:::note warn
ラベルを表示するレイヤの領域設定

ベクタ・レイヤで地名や建物名などのラベルを表示するものは、次の点に注意が必要です。

- `metabuffer` を大きく取らないと、タイル境界上でラベルが途切れることがある
- `Point` のレイヤ、または、外部にラベルを表示する `Polygon` のレイヤでは、領域自体もレイヤの `BBox` より広めに取る必要がある
:::

レイヤごとのグリッド設定も Excel の表にして記録するのがお奨めです。そうしないと、すぐに忘れて、何が何だか分らなくなります ... 少なくとも私は。