ArcGIS Online ベクトルタイル用のタイルマップのいくつかを静的コンテンツとして作ってみる

## はじめに
これまでに、GitHubページを使ってArcGIS OnlineのためのVectorTileServer（オーバーズーミング機能付き）を作りました（以前の記事は[こちら](https://qiita.com/T-ubu/items/d2454cd7b018820fa8b1)）。これまでの実験で、ある程度の静的なホスティング（GitHubページ）でも、オーバーズーミングを実装できそうなことを確認しました。

今回は、より大縮尺でオーバーズーミングを行うことを目指して、静的なホスティング（GitHubページ）で、ArcGIS REST APIの tilemap をどれだけ作れるか実験してみました。具体的にはシェルスクリプトを使って、tilemapサービス用のjsonファイルを準備することとしました。

## 作業環境
* Windows 10 (Enterprise)
* Docker version 20.10.8　　（シェルスクリプトを実行するためだけにつかいました）
* PowerShell version 5.1.19041.1237　　（シェルスクリプトを実行するためだけにつかいました）

## 方針

* フォルダの構造として ズームレベル/32n/32m/32/32 （m, nは整数）という構造の中にindex.jsonファイルを作っておく。
    * tilemapでは、~~tilemap/ズームレベル/列（左端の位置）/行（上端の位置）/ 横幅 / 縦幅~~ tilemap/ズームレベル/行（上端の位置）/列（左端の位置）/ 横幅 / 縦幅で問い合わせを行っている。 本来は任意のパスで応答するべきだが、ArcGIS Onlineの観察では問い合わせの範囲は常に32×32、上端・左端も32の倍数のみだった。特定のパスのフォルダを準備して、そこにindex.jsonファイルを作っておいておくことにする（index.jsonという名前にしておくことで、ディレクトリまでの指定でjsonが返せるようにする）
* 各ズームレベルにおいて、地区ごとにデータの有無を記載することはできないので、そのズームレベルにおいてすべてのタイルがある場合と、タイルが全くない場合いう両極端のケースについてそれぞれtilemapを作成する。
    * タイルがある場合は、32×32なので、帰ってくるjsonファイルの中のdataには1が1024個並んでいる（注:ズームレベル0～4はタイル数が1024以下なのでこの限りではない。）。
    * タイルがない場合は errorとして422を返すようです。
* ズームレベルが上がるにつれてファイル数が大きくなり、静的コンテンツとして処理しにくくなると思われる。どこまでできそうか確かめる。

## 参考にしたtilemap

左上が(0, 0)番目のタイルから始まる 32×32の区画にすべてタイルがある場合のtilemap。1024のタイルにすべてデータがあるので、1が1024個並んでいます。locationでleftとtopの位置を指定しないといけないのがポイントです。

```
{"adjusted":false,"location":{"left":0,"top":0,"width":32,"height":32},"data":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]}
```

指定（32×32でもなんでも）の区画にすべてタイルがない場合のtilemap。左と上の位置の指定は不要のようです。

```
{"error":{"code":422,"message":"Tiles not present","details":[]}}
```

## 作業
たくさんのフォルダを作ってファイルをコピーしたり加工したりするので、作業はシェルスクリプトを使ってやることにしました。Windows環境で作業しましたが、 疑似Linux環境を使うためDockerでunvt/nanbanを立ち上げました。
作業したものはGitHubレポジトリにアップしてあります。

### Step1: ZL 0～4
ズームレベル0-4については、タイルが存在するにしても範囲に注意が必要なので、手書きで以下のようなタイルマップを作りました。

（タイルが存在する場合）
https://github.com/ubukawa/tilemap-prototype01/blob/main/caseTile/0/0/0/32/32/index.json
https://github.com/ubukawa/tilemap-prototype01/blob/main/caseTile/1/0/0/32/32/index.json
https://github.com/ubukawa/tilemap-prototype01/blob/main/caseTile/2/0/0/32/32/index.json
https://github.com/ubukawa/tilemap-prototype01/blob/main/caseTile/3/0/0/32/32/index.json
https://github.com/ubukawa/tilemap-prototype01/blob/main/caseTile/4/0/0/32/32/index.json

（タイルが存在しない場合）
ファイル自体はすべて一緒ですが、フォルダ構造は以下のようなところに置いてあります。
https://github.com/ubukawa/tilemap-prototype01/blob/main/caseNoTile/0/0/0/32/32/index.json
https://github.com/ubukawa/tilemap-prototype01/blob/main/caseNoTile/1/0/0/32/32/index.json
https://github.com/ubukawa/tilemap-prototype01/blob/main/caseNoTile/2/0/0/32/32/index.json
https://github.com/ubukawa/tilemap-prototype01/blob/main/caseNoTile/3/0/0/32/32/index.json
https://github.com/ubukawa/tilemap-prototype01/blob/main/caseNoTile/4/0/0/32/32/index.json

### Step2: ZL5～
ズームレベル５を超えると、ズームレベルが上がるにつれて必要なファイル数が4倍になっていきますし、パスも規則的に準備できるので、スクリプトを書いて対応しました。

#### タイルがない場合
タイルがないときは簡単で、jsonファイルを入れるディレクトリを作ってあげて、そのなかに事前に準備しておいたerror422のjsonファイル(src/e422.jsonというパス)をコピーするだけでファイルを作りました。
以下の例では、forループを3回使っていて、1)ズームレベルz(5~13まで)、2)左端に関係する変数n(0以上 2^(z-5)未満)、3)上端に関係する変数m(0以上 2^(z-5)未満)があります。（スクリプトを見たほうが早いです。）

```bash:フォルダを作ってコピーするシェルスクリプト
#This script is for ZL 5 or larger than 5.
for ((z=5; z<14; z++)); do mkdir ${z}; echo ${z}; date; for((n=0; n<2**(z-5); n++)); do mkdir ${z}/$((n*32)) ; for((m=0; m<2**(z-5); m++)); do mkdir ${z}/$((n*32))/$((m*32)); mkdir ${z}/$((n*32))/$((m*32))/32; mkdir ${z}/$((n*32))/$((m*32))/32/32; cp src/e422.json ${z}/$((n*32))/$((m*32))/32/32/index.json; done; done; echo ${z} ok; date; done;
```
このファイルは https://github.com/ubukawa/tilemap-prototype01/blob/main/Nodata.sh にあります。ズームレベルを調整する場合は最初のzのforループのところのZの範囲を調整してください。

#### タイルがある場合
問い合わせをするタイルマップの範囲は32×32ですが、パスごとにlocationの左端と上端のタイルの番号（いわゆるxとyですね）が違います。location以外を含む以下のような不完全なjsonファイルを準備しておき、それをコピーした後に、シェルスクリプトのechoを使って、パスに応じたlocation（とjsonファイルを閉じる括弧）を追加することにしました。

```json:準備したjsonファイルのもと（最後の括弧はあえて閉じていません）
{"adjusted":false,"data":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
```

```bash:
for ((z=5; z<14; z++)); do mkdir ${z}; echo ${z}; date; for((n=0; n<2**(z-5); n++)); do mkdir ${z}/$((n*32)) ; for((m=0; m<2**(z-5); m++)); do mkdir ${z}/$((n*32))/$((m*32)); mkdir ${z}/$((n*32))/$((m*32))/32; mkdir ${z}/$((n*32))/$((m*32))/32/32; cp src/index00.json ${z}/$((n*32))/$((m*32))/32/32/index.json; n1=$((n*32)); m1=$((m*32)) ;echo \"location\":\{\"left\":${m1},\"top\":${n1},\"width\":32,\"height\":32\}\} >> ${z}/$((n*32))/$((m*32))/32/32/index.json; done; done; echo ${z} ok; date; done;
```
（2022.1.7 topとleftの順番を間違えていたのでbashのコマンドを微修正しました。）

このスクリプトは https://github.com/ubukawa/tilemap-prototype01/blob/main/Withdata4.sh にあります。ズームレベルを調整する場合は最初のzのforループのところのZの範囲を調整してください。また、コピー元のjsonファイル（[これです](https://github.com/ubukawa/tilemap-prototype01/blob/main/src/index00.json)）のパスは適宜調整してください。


## 結果
タイルがある場合に城、ない場合にしろ、ZL13までは静的コンテンツとしてjsonファイルを作成できました。以下に各ズームレベルでのtilemapのデータサイズと、私がスクリプトでファイルを作ったときの時間をメモしておきます。ズームレベル13までは上記の方法で何とか静的コンテンツとしてtilemapを準備できそうでした。
ズームレベル14を超えると、ファイルの数が多くなり、ファイルの生成、コピー、GitHubへのアップロード等で時間もかかりますし現実的ではないように思いました。

|Zoom Level  |number of tile<br> (Whole globe)   |  number of necessary tilemap (32 by 32)   |  Tilemap total size <br> Case of returning exist tile(1)  |  Processing time for creation (sec)  |  Tilemap total size <br> Case of returnign no tile exist (0)   |  Processing time for creation (sec)  |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|    
|0  |1   |1   |  2.5 (KB)  | n.a. |  512 (B)  | n.a. |
|1  |4   |1   |  2.5 (KB)  | n.a. |  512 (B)  | n.a. |
|2  |16   |1   |  2.5 (KB)  | n.a. |  512 (B)  | n.a. |
|3  |64   |1   |  2.5 (KB)  | n.a. |  512 (B)  | n.a. |
|4  |256   |1   |  2.5 (KB)  | n.a. |  512 (B)  | n.a. |
|5  |1,024   |1   |  2.5 (KB)  |  1  |  512 (B)  | 1 |
|6  |4,096   |4   | 10 (KB) |  1  |  2.0 (KB)  | 1 |
|7  |16,384   |16   | 40 (KB) |  2  |  8.0 (KB)  | 1 |
|8  |65,536   |64   | 196 (KB) |  6  |  68 (KB)  | 6 |
|9  |262,144   |256   | 708 (KB) |  25  |  196 (KB)  | 22 |
|10  |1,048,576   |1,024   | 2.7 (MB) |  98  |  644 (KB)  |  82  |
|11  |4,194,304   |4,096   | 11 (MB) | 374 |  2.6 (MB)  |  339  |
|12  |16,777,216   |16,384   | 43 (MB) | 1,457 |  11 (MB)  |  1,312  |
|13  |67,108,864   |65,536 | 173 (MB) | 6,565 |  45 (MB)  |  4,933  |
|14  |268,435,456   |262,144   | | | | |
|15  |1,073,741,824   |1,048,576   | | | | |
|16  |4,294,967,296   |4,194,304   | | | | |
|17  |17,179,869,184   |16,777,216   | | | | |
|18  |68,719,476,736   |67,108,864   | | | | |
|19  |274,877,906,944   |268,435,456   | | | | |
|20  |1,099,511,627,776   |1,073,741,824   | | | | |
|21  |4,398,046,511,104   |4,294,967,296   | | | | |
|22  |17,592,186,044,416   |17,179,869,184   | | | | |

## まとめ
ArcGIS　REST APIのtilemapは、本来は任意の範囲についてjsonを返す機能ですが、今回の試行では、32×32の範囲で左上の位置も32の倍数に限定し、tilemapの一部について静的なコンテンツとして作成することができました。
このような限定的なtilemapでもArcGIS Onlineで表示する際に、オーバーズーミングを効かせることができると思います（[こちらの記事](https://qiita.com/T-ubu/items/d2454cd7b018820fa8b1)）。

ただし、もう少し縮尺の大きいズームレベルに対応するためには、例えばnodejs/expressなどのサーバーでルーティングを使うとか、何らかの工夫がいるかなと思っており、今後の課題としたいです。

:::note info
（2022.1.7 追記）
当初、この記事でtilemapのパスのtopとleftの順番を反対に理解していました。すみません。
また、その後取り組みを進め、nodejs/expressのルーティングでtilemapを返すサーバーを作りました。レポジトリはこちら https://github.com/unvt/marble です。
:::

## 謝辞
ArcGIS REST APIを参考にさせていただきました。また、[ArcGIS Vector Basemaps] (https://www.arcgis.com/home/group.html?id=30de8da907d240a0bccd5ad3ff25ef4a#overview)を観察させていただきました。ありがとうございます。

## 参考
* [ArcGIS REST API - Tile Map](https://developers.arcgis.com/rest/services-reference/enterprise/tile-map.htm)
* [ArcGIS Vector Basemaps] (https://www.arcgis.com/home/group.html?id=30de8da907d240a0bccd5ad3ff25ef4a#overview)
* [作業したレポジトリ](https://github.com/ubukawa/tilemap-prototype01)