地理院タイルの地図を表示する

今回は[国土地理院](https://www.gsi.go.jp/)が公開している地理院タイルを使ってRubyで地図アプリを作ってみます。

地理院タイルの仕様は「[地理院タイルについて](https://maps.gsi.go.jp/development/siyou.html
)」を参考にします。
ざっくり言うとこんな感じになっています。
>https://cyberjapandata.gsi.go.jp/xyz/{t}/{z}/{x}/{y}.{ext}
>{t}：データID
>{x}：タイル座標のX値
>{y}：タイル座標のY値
>{z}：ズームレベル
>{ext}：拡張子

# Step1 タイル画像を表示してみる

まずは、[標準地図](https://maps.gsi.go.jp/development/ichiran.html#std)から一つだけダウンロードして表示してみます。

``` ruby:sample1.rb
require 'open-uri'
require 'dxruby'

url = "http://cyberjapandata.gsi.go.jp/xyz/std/7/110/51.png"
file = "51.png"
open(file, 'wb') do |path|
  URI.open(url) do |recieve|
    path.write(recieve.read)
  end
end

image = Image.load(file)

Window.loop do
  Window.draw(0, 0, image)
end
```
今回はWindows限定でDXRubyを使っていますが、png画像を読み込んで表示できるライブラリあれば何でも良いです。
DxRubyはコマンドラインで
``` bash
>gem install dxruby
```
でインストールできます。

:::note warn
以前はこれだけで実行できたのですが、Windows10からDll等が必要になるかもしれません。
https://qiita.com/noanoa07/items/0ce14c2404df38de94b7
:::


実行方法: コマンドラインから
``` bash
>ruby sample1.rb
```
で実行すると、ウィンドウが開いて地図画像が一枚表示されます。
![DXRuby Application 2022_10_09 20_44_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72890/74b15785-4b2a-0953-3d0f-2c238e871cab.png)


# Step2 タイルを並べてマップっぽく表示する

次に何枚か取得してタイル状に並べて地図っぽくしてみます。
また、ダウンロードした画像はローカルにキャッシュして少しでもサーバーに負荷をかけないようにします（ココ大事）。


``` ruby:sample2.rb
require 'open-uri'
require 'fileutils'
require 'dxruby'

def download(z: 0, x: 0, y: 0)
  url = "http://cyberjapandata.gsi.go.jp/xyz/std/#{z}/#{x}/#{y}.png"
  uri = URI.parse(url)
  file = Dir.pwd + uri.path
  unless FileTest.exist?(file)
    dir = File.dirname(file)
    FileUtils.mkdir_p(dir) unless FileTest.exist?(dir)
    open(file, 'wb') do |path|
      URI.open(uri) do |recieve|
        path.write(recieve.read)
      end
    end
  end
  file
end

images = []
2.times do |y|
  cols = []
  3.times do |x|
    file = download(z: 7, x: 110 + x, y: 51 + y)
    cols << Image.load(file)
  end
  images << cols
end

view_x = 0
view_y = 0

Window.loop do
  view_x += Input.x
  view_y += Input.y

  2.times do |y|
    3.times do |x|
      image = images[y][x]
      Window.draw(view_x + x * 256, view_y + y * 256, image)
    end
  end
end
```

![DXRuby Application 2022_10_10 19_41_27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72890/e2386713-0d7c-3474-c539-eeddada27bc1.png)

カーソルキーで上下左右にスクロールできるようにしたのですが、最初にダウンロードしたタイルのみしか表示されないので、端にくると真っ黒になっています。


# Step3 スクロールに合わせてタイルを表示させる
という訳で今度は画面のスクロールに合わせてリアルタイムに画像をダウンロードするように改良します。
仕組みは最初はスクロールを判定してダウンロードするようにプログラムしていたのですが、どうしてもコードが長く複雑になったので、発想を変えてワールド座標の画面に表示されている部分（カメラ）のみ表示（画像がなかったら取得）するようにしたらシンプルに実装できました。
（その為に一辺が2^ズームレベルの空配列を作っているのでズームレベルが高くなると注意）

``` ruby:sample3.rb
require 'open-uri'
require 'fileutils'
require 'dxruby'

def download(z: 0, x: 0, y: 0)
  url = "http://cyberjapandata.gsi.go.jp/xyz/std/#{z}/#{x}/#{y}.png"
  uri = URI.parse(url)
  file = Dir.pwd + uri.path
  unless FileTest.exist?(file)
    dir = File.dirname(file)
    FileUtils.mkdir_p(dir) unless FileTest.exist?(dir)
    open(file, 'wb') do |path|
      URI.open(uri) do |recieve|
        path.write(recieve.read)
      end
    end
  end
  file
end

MAX_WIDTH = 2 ** 7
MAX_HEIGHT = 2 ** 7
tiles = Array(MAX_HEIGHT * MAX_WIDTH)

view_x = 110 * 256
view_y = 51 * 256

Window.loop do
  view_x += Input.x
  view_y += Input.y

  (Window.height / 256 + 2).times do |j|
    (Window.width / 256 + 2).times do |i|
      y = view_y / 256 + j
      x = view_x / 256 + i
      index = y * MAX_WIDTH + x
      unless tiles[index]
        file = download(z: 7, x: x, y: y)
        tiles[index] = Image.load(file)
      end
      image = tiles[index]
      Window.draw(-(view_x % 256) + i * 256, -(view_y % 256) + j * 256, image)
    end
  end
end
```

![DXRuby Application 2022_10_10 19_42_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72890/8a806ddd-713b-a371-bb19-1dc4b6688c56.png)

これで縦横どこまでもスクロールできるようになりました。
非同期処理をしなくても割とスムーズに表示されます。国土地理院スゴイ
ただこのままだと表示されなくなった画像の削除（ガベージコレクト）はやっていないのでどんどんメモリを食っていくのですが。

という訳でこれだけのコードで地図アプリっぽくなりました。次はズームレベルの変更を実装してみたいです。