緯度経度から気象庁の天気予報用地域コードを取得するWebAPI

# 追記
herokuが有料化してからこのWebAPIは停止しています。
気象庁のJSONから希望の地域の天気予報を取得するための解説として読んでください。

# 気象庁の天気予報
2021年2月より、気象庁の天気予報がJSONとして取得できるようになりました。あくまでJSONが取得できるような構造になっているというだけで、正式にAPIとして公開しているわけではないそうです。
天気予報を取得したい地域の地域コードでリクエストを送ると、概要や3日間天気、週間天気などが取得できます。

しかしこの地域コード、自治体コードがベースになっており、あまりなじみがありません。
さらに、気象庁が公開している地域コード一覧JSONは（少なくともGoでは）非常に扱いにくい構造になっており、目的の地域コードを探すのも大変です。
これではせっかくプログラムで天気予報を取得しやすくなったのに、地域指定でつまづいてしまいます。

# 地域コードを取得するWebAPIを作ってみた

そこで、もっと簡単に目的の地域の天気予報を取得できるように、緯度経度から

* 天気予報リクエスト用コード
* コードに対応する市町村名
* レベル別地域コード

を取得できるようにしてみました。


```
https://revgeo-forecastcode.herokuapp.com/lat={lat}+lon={lon}
```

上記のURLの`{lat}`を緯度に、`{lon}`を経度に置き換えてhttpリクエストを送ると、以下のようなJSONを返します。


```:（例）京都御苑の緯度経度でリクエスト
https://revgeo-forecastcode.herokuapp.com/lat=35.021077+lon=135.761731
```
```json
{
  "forecastcode": "260000",
  "prefname": "京都府",
  "cityname": "京都市",
  "centers": "010600",
  "offices": "260000",
  "class10s": "260010",
  "class15s": "260011",
  "class20s": "2610000"
}
```
ここで取得できた`forecastcode`を利用すれば、気象庁から天気予報を取得できます。基本的には`offices`コードと同じものになりますが、北海道の十勝地方と鹿児島の奄美地方については、それぞれ釧路地方と鹿児島県のデータに統合されているので`offices`コードと`forecastcode`が異なります。

```:（例）京都府の天気概要
https://www.jma.go.jp/bosai/forecast/data/overview_forecast/260000.json
```


```:（例）京都府の3日間天気
https://www.jma.go.jp/bosai/forecast/data/forecast/260000.json
```

3日間天気のデータにはその都道府県内の各地域の天気予報が含まれています。希望の地域のデータを抽出するために`class10s`コード等を使用します。
天気予報データの中身については他にも詳しい記事がたくさんあるので省略します。

# 処理の中身
#### 緯度経度から自治体コードへ変換
緯度経度から自治体コードへの変換は国土地理院の逆ジオコーディングAPIを利用しました。

```
https://mreversegeocoder.gsi.go.jp/reverse-geocoder/LonLatToAddress?lat={lat}&lon={lon}
```
このAPIにより市レベル、または特別区や政令市においては区レベルの自治体コードが得られます。
<details><summary>ソース</summary><div>

```go
type RevGeo struct {
	Results struct {
		MuniCd string `json:"muniCd"`
		Lv01Nm string `json:"lv01Nm"`
	} `json:"results"`
}

func latlonToLocalCode(lat, lon float64) string {
	URL := fmt.Sprintf("https://mreversegeocoder.gsi.go.jp/reverse-geocoder/LonLatToAddress?lat=%v&lon=%v", lat, lon)
	body, err := common.GetJson(URL)
	common.ErrLog(err)

	var rg RevGeo
	json.Unmarshal(body, &rg)

	return rg.Results.MuniCd
}
```
</div></details>

#### 自治体コードを市レベルのコードに変換
気象庁の地域コードで利用されているのはもう少し荒い市レベルの自治体コードですので、区レベルのコードを市レベルに変換します。
総務省が[市レベルの自治体コード表を公開しています](https://www.soumu.go.jp/denshijiti/code.html)ので、これを利用して近似値検索をすることで市レベルにできます。区レベルコードは市レベルコードよりも大きい数字ですので、直近下位を探します。
ちなみに、自治体コードの6桁目はただのチェックディジットですので、CSVに加工するときに切り落としました。
<details><summary>ソース</summary><div>

```go
// 区レベルの自治体コードを市レベルに直す
// 二分探索で近似値検索をしている
func localCodeToCityCode(code string) string {
	codes := loadCityCodes()
	left, right := 0, len(codes)-1
	var mid int
	for right-left > 1 {
		mid = (left + right) / 2
		if code == codes[mid] {
			break
		} else if code > codes[mid] {
			left = mid
		} else {
			right = mid
		}
	}
	if code < codes[mid] {
		mid--
	}
	return codes[mid]
}

var cityCodes []string

// 総務省が公開している市レベル自治体コード表をCSVにしたものを読み込む
func loadCityCodes() []string {
	if len(cityCodes) != 0 {
		return cityCodes
	}
	file, err := os.Open("./code.csv")
	common.ErrLog(err)
	defer file.Close()

	reader := csv.NewReader(file)
	for {
		line, err := reader.Read()
		if err != nil {
			break
		}
		cityCodes = append(cityCodes, line[0])
	}
	return cityCodes
}

```
</div></details>

#### 地域コードに変換
気象庁の地域コード一覧を取得します。

```
https://www.jma.go.jp/bosai/common/const/area.json
```
1万7千行もあるうえに地域コードが配列にもなっていません。
GoではJSONを読み込むときに構造体を作ってそこにマップするのですが，この形式では構造体の定義も1万7千行になってしまいました。
ものは試しにやってみましたがサイズに耐えられなかったのかやはりエラーが。
どうしたものかと悩んでいると[go-dproxy](https://github.com/koron/go-dproxy)という便利そうなライブラリを発見。JSONはmap[string]interface{}型（辞書型）にできることを利用してキーから要素にアクセスするようです。

https://www.kaoriya.net/blog/2016/06/25/

気象庁の地域コードは`centers, offices, class10s, class15s, class20s`という階層に分かれています。

|階層名|対応する階層|例|
|:-:|:-:|:-:|
|centers|地方|近畿地方|
|offices|都道府県|京都府|
|class10s|位置|南部|
|class15s|代表都市|京都・亀岡|
|class20s|市町村|京都市|

`class20s`コードが先の自治体コードの末尾に`"00"`を付加したものになっています。
各地域コードには親コード・子コードが記載されているので，`class20s`コードが分かれば`centers`コードまでたどっていけます。
<details><summary>ソース</summary><div>

```go
func parentcode(code string) string {
	category := []string{"class20s", "class15s", "class10s", "offices"}
	areainfo := dproxy.New(area.AreaInfoMap())
	for _, class := range category {
		pcode, err := areainfo.M(class).M(code).M("parent").String()
		if err == nil {
			return pcode
		}
	}
	return ""
}

func toCityName(citycode string) string {
	areainfo := dproxy.New(area.AreaInfoMap())
	name, _ := areainfo.M("class20s").M(citycode).M("name").String()
	return name
}
func toPrefName(officecode string) string {
	areainfo := dproxy.New(area.AreaInfoMap())
	name, _ := areainfo.M("offices").M(officecode).M("name").String()
	return name
}
```
</div></details>

#### レスポンス

URLに含まれる変数を取得するやり方を知らなかったのでググりました。

https://hodalog.com/creating-restfulapi-with-golang/

ここで紹介されている[gorilla/mux](https://github.com/gorilla/mux)を使って処理します。
あとは例外地域の処理をしてJSONで返事をするだけです。

<details><summary>ソース</summary><div>

```go
func main() {
	r := mux.NewRouter()
	r.HandleFunc("/lat={lat}+lon={lon}", procRequest)
	log.Fatal(http.ListenAndServe(":"+os.Getenv("PORT"), r))
}

type Results struct {
	ForecastCode string `json:"forecastcode"`
	PrefName     string `json:"prefname"`
	Cityname     string `json:"cityname"`
	Centers      string `json:"centers"`
	Offices      string `json:"offices"`
	Class10s     string `json:"class10s"`
	Class15s     string `json:"class15s"`
	Class20s     string `json:"class20s"`
}

func procRequest(w http.ResponseWriter, req *http.Request) {
	vars := mux.Vars(req)
	lat, err := strconv.ParseFloat(vars["lat"], 64)
	common.ErrLog(err)
	lon, err := strconv.ParseFloat(vars["lon"], 64)
	common.ErrLog(err)
	localcode := latlonToLocalCode(lat, lon)

	var res Results
	if localcode != "" {
		citycode := localCodeToCityCode(localcode)
		res.Class20s = citycode + "00"
		res.Class15s = parentcode(res.Class20s)
		res.Class10s = parentcode(res.Class15s)
		res.Offices = parentcode(res.Class10s)
		res.Centers = parentcode(res.Offices)
		switch res.Offices {
		case "014030":
			res.ForecastCode = "014100"
		case "460040":
			res.ForecastCode = "460100"
		default:
			res.ForecastCode = res.Offices
		}
		res.PrefName = toPrefName(res.Offices)
		res.Cityname = toCityName(res.Class20s)
	}
	json.NewEncoder(w).Encode(res)
}
```
</div></details>