C# WinFormsで地図描画

[地震界隈 Advent Calendar 2021](https://adventar.org/calendars/6729) 12日目です。
#はじめに
最近の地震ソフト、例えば [KyoshinEewViewerIngen](http://svs.ingen084.net/kyoshineewviewer/), [JQuake](https://jquake.net/) などには、"イケてる"地図が使用されています。僕もそんなイケてる地図を作るべく、自分なりに色々調べたので共有したいと思います。

![ss.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/957258/25316017-d10c-45f7-3b20-7347560abcdf.gif)
![E_ZcXazVQAINJI2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/957258/555efb5f-9149-8822-0f70-850921f04ead.jpeg)
![台風.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/957258/6b4f865e-6658-d7af-cb6e-65ecf0265c46.png)

作成した地図では、[KyoshinMonitorLib](https://github.com/ingen084/KyoshinMonitorLib)を使用した[強震モニタ](http://www.kmoni.bosai.go.jp/)の観測点や、台風の予報円なども描画させることが出来ました。

・[ライブラリ未使用で地図描画入門編(WPF)](https://qiita.com/ingen084/items/a01409890f505c962327)を参考にしました。目を通したほうが理解しやすいです。
・かなり浅い知識で記述しているので間違いや改善するべき点などありましたらコメントにお願いいたします。


#目次
- [考え方](#考え方)
- [GeoJsonからデータを取り出す](#geojsonからデータを取り出す)
- [地理座標とピクセル座標の変換](#地理座標とピクセル座標の変換)
- [ピクセル座標から画面座標に変換](#ピクセル座標から画面座標に変換)


#考え方
全体の流れを図にしました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/957258/52cb9e7a-0de3-4748-d52c-6ee917b35356.png" width=300>
※GeoJson: GISデータ(地図データ)をJsonで記述したもの。
※地理座標: 緯度経度
※ピクセル座標: 投影法によって変換された座標
※画面座標: ウィンドウの座標

注意するべき部分は、地理座標からピクセル座標に変換したまま描画してしまうと、ウィンドウの画面外に表示されてしまう為、ウィンドウの中心座標(持っていきたい座標)へ変換させる必要があることです。「ピクセル座標から画面座標へ変換」はこのための処理です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/957258/4c2e062a-9079-feb7-5763-32b1dc1128fd.png" width=500>

#GeoJsonからデータを取り出す
地図を描画する前に、地図のデータを読み込む必要があります。
今回使わせていただいたGeoJsonのデータは[こちら](https://japonyol.net/editor/article/47-prefectures-geojson.html)です。
GeoJsonは内容はこのような感じです。

```json
/*一部省略*/
{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "geometry": {
                "type": "MultiPolygon",
                 "coordinates": [
                     [
                         [
                            [142.58922,44.938942],
                            [142.578181,44.961473],
                            //下に続く
                         ],
                         [
                            [139.356526,41.522899],
                            [139.356526,41.522899],
                            //下に続く
                         ]
                     ],
                 ]
            }
       },
       {
            "type": "Feature",
            "geometry": {
                "type": "Polygon",
                "coordinates": [
                    [
                        [139.356526,41.522899],
                        [139.356526,41.522899],
                        //下に続く
                    ]
                ]
            }
       },
    ]
}
```
このGeoJsonから`coordinates`にある地理座標をすべて取り出す必要があります。
また、GeoJsonでは`MultiPolygon`と`Polygon`とで含まれる配列の数が違う点に注意します。
c#でJsonファイルを扱う為に、`Newtonsoft.Json`と呼ばれるライブラリをしました。
`JObject.Parse()`関数を使うと、わざわざクラスを作らなくてもJsonを解析することができます。

```c#:c#
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
//↑これらを読み込む
```

```c#:c#
//処理
var json = JObject.Parse(JsonString); // GeoJsonの文字列を引数に入れる。
var features = json["features"];
foreach(var item in features){
    var type = (string)item["geometry"]["type"];
}
```

#地理座標とピクセル座標の変換
地理座標からピクセル座標に変換するための投影法は、[Webメルカトル座標系](https://club.informatix.co.jp/?p=1183)を使用しています。
また、Google Maps APIで使用されいてる?変換式を使うことにしました。[こちら](http://hosohashi.blog59.fc2.com/blog-entry-5.html)を参考。

**経度α, 緯度βからピクセル座標を計算**

```math
x = 2^{z+7}(\frac{\alpha}{180}+1)
\\
y = \frac{2^{z+7}}{\pi}(-tanh^{-1}(sin(\frac{\pi }{180}\beta)) + tanh^{-1}(sin(\frac{\pi }{180}L)))
\\
L = 85.05112878
\\
z = ZoomLevel
```

**ピクセル座標から経度α,緯度βを計算**

```math
\alpha = 180(\frac{x}{2^{z+7}}-1)
\\
\beta = \frac{180}{\pi }(sin^{-1}(tanh(-\frac{\pi }{2^{x+7}}y + tanh^{^-1}(sin(\frac{\pi }{180}L)))))
\\
L = 85.05112878
\\
z = ZoomLevel
```
これらをc#で再現します。

```c#:c#
public static class MapConverter
{
    const double L = 85.05112878;

    /// <summary>
    /// 地理座標からピクセル座標に変換する
    /// </summary>
    /// <param name="Latlon">地理座標</param>
    /// <param name="zoom">ズームレベル</param>
    public static PointDouble LatlonToPixel(Latlon latlon, double zoom)
    {
        var x = Math.Pow(2, zoom + 7) * ((latlon.Latitude / 180) + 1);

        var y = (Math.Pow(2, zoom + 7) / Math.PI) *

                ((-1 * (atanh(Math.Sin((Math.PI / 180) * latlon.Longitude)))) +
                    
                (atanh(Math.Sin((Math.PI / 180) * L))));

        return new PointDouble(x, y);
    }

    /// <summary>
    /// ピクセル座標から地理座標に変換する
    /// </summary>
    public static Latlon PixelToLatlon(PointDouble pd, double zoom)
    {
        var lon = 180 * (pd.X / (Math.Pow(2, zoom + 7)) - 1);

        var lat = (180 / Math.PI) * 
                  (Math.Asin(Math.Tanh(
                      ((-1 * ((Math.PI/Math.Pow(2, zoom + 7)) * pd.Y)) +
                      (atanh(Math.Sin((Math.PI / 180) * L))))
                  )));

        return new Latlon(lat, lon);
    }

    /// <summary>
    /// 逆双曲線正接
    /// </summary>
    public static double atanh(double x)
    {
        return (1d / 2d * Math.Log((1 + x) / (1 - x), Math.E));
    }
}
```

```c#:PointDouble.cs
public class PointDouble
{
    public double X, Y;
    public PointDouble(double x, double y)
    {
        this.X = x;
        this.Y = y;
    }

    public PointDouble Diff(PointDouble x)
    {
        return new PointDouble(this.X - x.X, this.Y - x.Y);
    }
    public PointDouble DiffAbs(PointDouble x)
    {
        return new PointDouble(Math.Abs(this.X - x.X), Math.Abs(this.Y - x.Y));
    }
}
```


```c#:Latlon.cs
public class Latlon
{
    /// <summary>
    /// Lat:緯度, Lon:経度
    /// </summary>
    public double Latitude, Longitude;

    public Latlon(double lat, double lon)
    {
        this.Latitude = lat;
        this.Longitude = lon;
    }
}
```

#ピクセル座標から画面座標に変換
[考え方](#考え方)でも述べたように、ピクセル座標からウィンドウ座標に変換しなければいけません。
例えば、東京の地理座標(35.681737, 139.767116)をウィンドウの中心へ移動させる場合、
ウィンドウの中心の座標と東京のピクセル座標の長さを全体から引くことで全体を中心に移動させることができます。

```c#:c#
//ズームレベル
var zoom = 5;
//ウィンドウの中心
var center = new PointDouble(Width / 2, Height / 2);
//東京の地理座標(ウィンドウの中心に持っていきたい座標)
var tokyo_latlon = new Latlon(35.681737, 139.767116);
//東京のピクセル座標
var tokyo_pixel = map.LatlonToPoint(tokyo_latlon, zoom);
//ウィンドウの中心と東京のピクセル座標の差
var diff = center.DiffAbs(tokyo_pixel);
//このdiffの値をGeoJsonから取得した地理座標をピクセル座標に変換した値から引く。
```

#最後に
僕が躓いた部分を中心に記述しました。
Formsで地図描画は向いてないかもしれない...