気象庁APIで得られた天気情報をアプリケーションで活用する

## はじめに
この記事で気象庁データを溜め込むまでを書いた。この記事はそれの活用である

https://qiita.com/YoshitakaOkada/items/1046fd83ef5c295491f1

適用するのはおなじみ、ポートフォリオ（soil_analysis）だ

https://github.com/duri0214/portfolio/tree/master/soil_analysis

## 改修の方針
このアプリは、圃場（畑とか田んぼとかを抽象化した名前）の場所を取り扱うので天気予報にぴったりだ。いまは住所を手入れしているがまぁ圃場の場所だからな、個人でいくつかあるわけだしいちいち住所をいれるのは面倒だろうから、エンハンス（＝改修）として
- 圃場の登録時・編集時に、緯度経度だけの入力とする
- 緯度経度から住所を特定する（市町村レベルまででよいので無料のツールを利用する）
- 圃場の住所を使って天気を表示する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/20e2e2ed-67c7-fae6-2cc9-b6bd8d4f3a85.png)

## Yahoo!リバースジオコーダAPI
緯度経度から住所を取得するものを逆ジオコーディングという（住所から緯度経度を取得するものをジオコーディングという）

https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/reversegeocoder.html

### 登録から使用の手順
まあgcp使うときもアプリケーションの登録はするわけだし、ええやろ

1. Yahoo! JAPAN IDを取得
1. アプリケーションを登録
   1. ID連携利用有無: ID連携を利用しない（YOLPを使用するだけ）
   1. 利用者情報: 個人
   1. 個人情報提供先としてユーザーへ開示することに同意しますか？: 同意しない（[見た感じ大丈夫そう](https://developer.yahoo.co.jp/changelog/2022-02-07-yconnect8.html) なので、まぁこれでやってみよう）
   1. アプリケーション名: soil analysis
   1. 登録完了！<br><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/0a3f6467-75fb-1293-9242-0e5a765e8f6f.png">
1. APIドキュメントを読む
1. アプリケーションを作成する
1. アプリケーションを公開する

https://developer.yahoo.co.jp/start/

## 手に入れたAPIキーを.envにまとめる
```conf:.env.example
    :
YAHOO_CLIENT_ID=
```
```conf:.env
    :
YAHOO_CLIENT_ID=****
```

## 緯度経度から住所を特定する
https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/reversegeocoder.html

### リクエストURL
https://map.yahooapis.jp/geoapi/V1/reverseGeoCoder

### 緯度経度の入力から住所を取得
都道府県、市区町村、残り住所　に住所をセパレートできる

#### vo
```py:soil_analysis/domain/valueobject/geocode/yahoo.py
from dataclasses import dataclass


@dataclass
class YDF:
    """
    Class YDF

    Represents a YDF object.

    Attributes:
        result_info (ResultInfo): Information about the results of the YDF query.
        feature (Feature): Information about a particular feature returned by the YDF query.
    """

    @dataclass
    class ResultInfo:
        """Class to store information about a result.

        Attributes:
            count (int): The count of results.
            total (int): The total number of results.
            start (int): The starting index of the results.
            latency (float): The latency of the query.
            status (int): The status code of the query.
            description (str): The description of the query.
        """

        count: int
        total: int
        start: int
        latency: float
        status: int
        description: str

    @dataclass
    class Feature:
        """class Feature:

        This class represents a feature with various attributes.

        Args:
            geometry (Feature.Geometry): The geometry of the feature.
            country (Feature.Country): The country where the feature is located.
            address_full (str): The full address of the feature.
            prefecture (Optional[Feature.Prefecture]): The prefecture of the feature, if available.
            city (Optional[Feature.City]): The city of the feature, if available.
            detail (Optional[Feature.Detail]): The detail of the feature, if available.

        Attributes:
            geometry (Feature.Geometry): The geometry of the feature.
            country (Feature.Country): The country where the feature is located.
            address_full (str): The full address of the feature.
            prefecture (Optional[Feature.Prefecture]): The prefecture of the feature, if available.
            city (Optional[Feature.City]): The city of the feature, if available.
            detail (Optional[Feature.Detail]): The detail of the feature, if available.
        """

        @dataclass
        class Geometry:
            """a geometric shape

            Args:
                type (str): The type of the geometric shape.
                coordinates (str): The coordinates of the shape.

            Attributes:
                type (str): The type of the geometric shape.
                coordinates (str): The coordinates of the shape.
            """

            type: str
            coordinates: str

        @dataclass
        class Country:
            """a country

            Attributes:
                code (str): The country code.
                name (str): The name of the country.
            """

            code: str
            name: str

        @dataclass
        class AddressElement:
            """Represents an element of an address

            Attributes:
                name (str): The name of the address element.
                kana (str): The kana representation of the address element.
                level (str): The level of the address element.
                code (Optional[str]): The code of the address element, if available.
            """

            name: str
            kana: str
            level: str
            code: str = None

        @dataclass
        class Prefecture(AddressElement):
            """a prefecture"""

            def __init__(self, name: str, kana: str, code: str = None):
                super().__init__(name=name, kana=kana, level="prefecture", code=code)

        @dataclass
        class City(AddressElement):
            """a city"""

            def __init__(self, name: str, kana: str, code: str = None):
                super().__init__(name=name, kana=kana, level="city", code=code)

        @dataclass
        class Detail(AddressElement):
            """address detail"""

            def __init__(self, name: str, kana: str, code: str = None):
                super().__init__(name=name, kana=kana, level="detail", code=code)

        geometry: Geometry
        country: Country
        address_full: str
        prefecture: Prefecture
        city: City
        detail: Detail

    result_info: ResultInfo
    feature: Feature
    
```

#### service
- `get_ydf_from_coords()`
    - 緯度経度の情報を持って yahoo reverseGeoCoder にアクセス `_fetch_xml()`
    - 得られたxmlのパース `_xml_to_ydf()`
- 気象庁のcityレコードを取得する `get_jma_city()`

```py:soil_analysis/domain/service/geocode/yahoo.py
import os
import re
import xml.etree.ElementTree as et

import requests

from soil_analysis.domain.valueobject.coords.googlemapcoords import GoogleMapCoords
from soil_analysis.domain.valueobject.geocode.yahoo import YDF
from soil_analysis.models import JmaCity


class ReverseGeocoderService:
    @staticmethod
    def get_ydf_from_coords(coords: GoogleMapCoords) -> YDF:
        """
        Args:
            coords: The GoogleMapCoords
        Returns:
            An instance of the YDF obtained from the GoogleMapCoords
        """
        xml_str = ReverseGeocoderService._fetch_xml(coords)
        return ReverseGeocoderService._xml_to_ydf(xml_str)

    @staticmethod
    def get_jma_city(ydf: YDF) -> JmaCity:
        """
        YDF地名を使って、対応するJmaCityオブジェクトを返します

        地名が "郡" が含まれる場合、"郡" よりうしろの文字列を取り出します "虻田郡倶知安町 → 倶知安町"
        地名が "市" が含まれる場合、"市" までの文字列を取り出します "浜松市中央区 → 浜松市"
        地名が "市" や "郡" を含まない場合、全ての文字列を取り出します。
        そうして取り出した文字列で、JmaCityの検索を行う
        （もしかしたら文字列操作があまいところがあるかもしれない）

        Args:
            ydf: The YDF
        Returns:
            JmaCity: The JmaCity object via city name in the YDF object
        """
        match = re.search(r"郡(.+)", ydf.feature.city.name)
        if match is not None:
            city_name = match.group(1)
        else:
            match = re.search(r"(.+市)", ydf.feature.city.name)
            if match is not None:
                city_name = match.group(1)
            else:
                city_name = ydf.feature.city.name

        return (
            JmaCity.objects.select_related("jma_region__jma_prefecture")
            .filter(name__contains=city_name)
            .first()
        )

    @staticmethod
    def _fetch_xml(coords: GoogleMapCoords) -> str:
        params = {
            "lat": coords.latitude,
            "lon": coords.longitude,
            "appid": os.environ.get("YAHOO_CLIENT_ID"),
            "datum": "wgs",  # 世界測地系（デフォルト）
            "output": "xml",  # XML形式（デフォルト）
        }

        request_url = "https://map.yahooapis.jp/geoapi/V1/reverseGeoCoder"
        response = requests.get(request_url, params=params)
        response.raise_for_status()

        return response.text

    @staticmethod
    def _xml_to_ydf(xml_str: str) -> YDF:
        """
        Converts an XML string to a YDF (Yahoo Data Format) object.

        Args:
            xml_str: A string containing the XML data.

        Returns:
            A YDF object containing the converted data.
        """
        ns = {"ns": "http://olp.yahooapis.jp/ydf/1.0"}

        tree = et.ElementTree(et.fromstring(xml_str))
        root = tree.getroot()

        result_info_element = root.find("ns:ResultInfo", ns)

        result_info = YDF.ResultInfo(
            count=int(result_info_element.find("ns:Count", ns).text),
            total=int(result_info_element.find("ns:Total", ns).text),
            start=int(result_info_element.find("ns:Start", ns).text),
            latency=float(result_info_element.find("ns:Latency", ns).text),
            status=int(result_info_element.find("ns:Status", ns).text),
            description=result_info_element.find("ns:Description", ns).text,
        )

        feature_element = root.find("ns:Feature", ns)

        country_element = feature_element.find("ns:Property/ns:Country", ns)
        country = YDF.Feature.Country(
            code=country_element.find("ns:Code", ns).text,
            name=country_element.find("ns:Name", ns).text,
        )

        geometry_element = feature_element.find("ns:Geometry", ns)
        geometry = YDF.Feature.Geometry(
            type=geometry_element.find("ns:Type", ns).text,
            coordinates=geometry_element.find("ns:Coordinates", ns).text,
        )

        address_full = feature_element.find("ns:Property/ns:Address", ns).text

        address_elements = feature_element.findall("ns:Property/ns:AddressElement", ns)
        first, second, *remaining = address_elements
        prefecture = YDF.Feature.Prefecture(
            name=first.find("ns:Name", ns).text,
            kana=first.find("ns:Kana", ns).text,
            code=(
                first.find("ns:Code", ns).text
                if first.find("ns:Code", ns) is not None
                else None
            ),
        )
        city = YDF.Feature.City(
            name=second.find("ns:Name", ns).text,
            kana=second.find("ns:Kana", ns).text,
            code=(
                second.find("ns:Code", ns).text
                if second.find("ns:Code", ns) is not None
                else None
            ),
        )
        detail_elements = [
            {
                "name": element.find("ns:Name", ns).text,
                "kana": element.find("ns:Kana", ns).text,
                "code": (
                    element.find("ns:Code", ns).text
                    if element.find("ns:Code", ns)
                    else None
                ),
            }
            for element in remaining
        ]
        detail = YDF.Feature.Detail(
            name="".join(
                [elem["name"] for elem in detail_elements if elem["name"] is not None]
            ),
            kana="".join(
                [elem["kana"] for elem in detail_elements if elem["kana"] is not None]
            ),
            code="".join(
                [elem["code"] for elem in detail_elements if elem["code"] is not None]
            ),
        )

        feature = YDF.Feature(
            geometry=geometry,
            country=country,
            address_full=address_full,
            prefecture=prefecture,
            city=city,
            detail=detail,
        )

        return YDF(result_info=result_info, feature=feature)
        
```

#### test
```py:soil_analysis/tests/domain/service/geocode/test_yahoo.py
from unittest import mock
from unittest.mock import patch

from django.test import TestCase

from soil_analysis.domain.service.geocode.yahoo import ReverseGeocoderService
from soil_analysis.domain.valueobject.coords.googlemapcoords import GoogleMapCoords


class TestGetYdfFromCoords(TestCase):
    @patch("requests.get")
    def test_get_ydf_from_coords(self, mock_get):
        mock_response = mock.Mock()
        mock_response.text = """
        <YDF xmlns="http://olp.yahooapis.jp/ydf/1.0" totalResultsReturned="1">
            <ResultInfo>
                <Count>1</Count>
                <Total>1</Total>
                <Start>1</Start>
                <Latency>0.18004202842712</Latency>
                <Status>200</Status>
                <Description>指定の地点の住所情報を取得する機能を提供します。</Description>
                <CompressType/>
            </ResultInfo>
            <Feature>
                <Property>
                    <Country>
                        <Code>JP</Code>
                        <Name>日本</Name>
                    </Country>
                    <Address>東京都港区赤坂９丁目７－１</Address>
                    <AddressElement>
                        <Name>東京都</Name>
                        <Kana>とうきょうと</Kana>
                        <Level>prefecture</Level>
                        <Code>13</Code>
                    </AddressElement>
                    <AddressElement>
                        <Name>港区</Name>
                        <Kana>みなとく</Kana>
                        <Level>city</Level>
                        <Code>13103</Code>
                    </AddressElement>
                    <AddressElement>
                        <Name>赤坂</Name>
                        <Kana>あかさか</Kana>
                        <Level>oaza</Level>
                    </AddressElement>
                    <AddressElement>
                        <Name>９丁目</Name>
                        <Kana>９ちょうめ</Kana>
                        <Level>aza</Level>
                    </AddressElement>
                    <AddressElement>
                        <Name>７</Name>
                        <Kana>７</Kana>
                        <Level>detail1</Level>
                    </AddressElement>
                    <Building>
                        <Id>B@iXzXO-G3A</Id>
                        <Name>ミッドタウン・タワー</Name>
                        <Floor>54</Floor>
                        <Area>5147</Area>
                    </Building>
                </Property>
                <Geometry>
                    <Type>point</Type>
                    <Coordinates>139.73134257366763,35.666049811559205</Coordinates>
                </Geometry>
            </Feature>
        </YDF>
        """
        mock_get.return_value = mock_response

        coords = GoogleMapCoords(latitude=35.681236, longitude=139.767125)
        ydf = ReverseGeocoderService.get_ydf_from_coords(coords)

        assert ydf.result_info.count == 1
        assert ydf.result_info.total == 1
        assert ydf.result_info.start == 1
        assert abs(ydf.result_info.latency - 0.18004202842712) < 1e-6
        assert ydf.result_info.status == 200
        assert (
            ydf.result_info.description
            == "指定の地点の住所情報を取得する機能を提供します。"
        )

        assert ydf.feature.geometry.type == "point"
        assert (
            ydf.feature.geometry.coordinates == "139.73134257366763,35.666049811559205"
        )

        assert ydf.feature.country.code == "JP"
        assert ydf.feature.country.name == "日本"

        assert ydf.feature.address_full == "東京都港区赤坂９丁目７－１"

        assert ydf.feature.prefecture.name == "東京都"
        assert ydf.feature.prefecture.kana == "とうきょうと"
        assert ydf.feature.prefecture.level == "prefecture"
        assert ydf.feature.prefecture.code == "13"

        assert ydf.feature.city.name == "港区"
        assert ydf.feature.city.kana == "みなとく"
        assert ydf.feature.city.level == "city"
        assert ydf.feature.city.code == "13103"

        assert ydf.feature.detail.name == "赤坂９丁目７"
        assert ydf.feature.detail.kana == "あかさか９ちょうめ７"
        assert ydf.feature.detail.level == "detail"
        
```

## 圃場の登録時に、緯度経度だけの入力とする
（更新するページはいまのところ作っていないので省略）

### models.py
- もとのコードでjmaシリーズのモデルがLandのモデルの下にあったので上に持ってきたけど、その差分はさすがに書いていない

```diff_python:soil_analysis/models.py
    :
class Land(models.Model):
    """
    圃場マスタ
    name        圃場名
-   prefecture  都道府県    e.g. 茨城県
-   location    住所       e.g. 結城郡八千代町
+   jma_city    市区町村       e.g. 八千代町
    latlon      緯度経度    e.g. 36.164677272061,139.86772928159
    area        面積       e.g. 100㎡
    image       写真
	@@ -108,9 +249,9 @@ class Land(models.Model):
    """

    name = models.CharField(max_length=256)
-   prefecture = models.CharField(max_length=256)
-   location = models.CharField(max_length=256)
+   jma_city = models.ForeignKey(JmaCity, on_delete=models.CASCADE)
-   latlon = models.CharField(null=True, blank=True, max_length=256)
+   latlon = models.CharField(max_length=256)
    area = models.FloatField(null=True, blank=True)
    image = models.ImageField(upload_to="land/", null=True, blank=True)
    remark = models.TextField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(null=True)
    company = models.ForeignKey(Company, on_delete=models.CASCADE)
    cultivation_type = models.ForeignKey(CultivationType, on_delete=models.CASCADE)
    owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
        :
```

### form
- 必須入力がわかりにくいので `*` つけた
- 名称重複をはじくようにした
- 緯度経度から住所が取得できない場合はエラーになっちゃうし、「中央区」とかだと東京の中央区になったりするから、確実な入力にするなら都道府県と市区町村は手入力（ドロップダウンから選択）がいいんだよなあ
- いちおう緯度経度を入力してフォーカスが外れたらapiから住所もってきてその場で確認（取得できなかったらドロップダウンが出てくるみたいな）というのもつけたら便利そうだけど

```diff_python:soil_analysis/forms.py
from django import forms
from django.forms import ClearableFileInput

from .models import Company, Land, JmaPrefecture, JmaCity


class CompanyCreateForm(forms.ModelForm):
    class Meta:
        model = Company
        fields = ("name", "image", "remark", "category")
        widgets = {
            "name": forms.TextInput(attrs={"class": "form-control", "tabindex": "1"}),
            "image": forms.ClearableFileInput(
                attrs={"class": "form-control", "tabindex": "2"}
            ),
            "remark": forms.TextInput(attrs={"class": "form-control", "tabindex": "3"}),
            "category": forms.Select(attrs={"class": "form-control", "tabindex": "4"}),
        }
        labels = {
            "name": "圃場名",
            "image": "画像",
            "remark": "備考",
            "category": "カテゴリー",
        }

    def clean_name(self):
        name = self.cleaned_data["name"]
        if "クサリク" in name:
            raise forms.ValidationError(
                "「クサリク」を含む取引先は登録できなくなりました（取引停止）"
            )

        return name


class LandCreateForm(forms.ModelForm):
    jma_prefecture = forms.ModelChoiceField(
        queryset=JmaPrefecture.objects.all(), empty_label="選択してください"
    )
    jma_city = forms.ModelChoiceField(
        queryset=JmaCity.objects.all(), empty_label="選択してください"
    )

    class Meta:
        model = Land
        fields = (
            "name",
            "latlon",
            "jma_prefecture",
            "jma_city",
            "area",
            "image",
            "remark",
            "cultivation_type",
            "owner",
        )
        widgets = {
            "name": forms.TextInput(attrs={"class": "form-control", "tabindex": "1"}),
            "latlon": forms.TextInput(
                attrs={
                    "class": "form-control",
                    "tabindex": "2",
                    "placeholder": "例: 35.658581,139.745433",
                }
            ),
            # TODO: なぜか bootstrap が反映されない（react化で解決したほうがよさそう）
            "jma_prefecture": forms.Select(
                attrs={
                    "class": "form-control",
                    "tabindex": "3",
                    "placeholder": "例: 兵庫県",
                }
            ),
            # TODO: なぜか bootstrap が反映されない（react化で解決したほうがよさそう）
            "jma_city": forms.Select(
                attrs={
                    "class": "form-control",
                    "tabindex": "4",
                    "placeholder": "例: 姫路市",
                }
            ),
            "area": forms.TextInput(
                attrs={
                    "class": "form-control",
                    "tabindex": "5",
                    "placeholder": "例: 100",
                }
            ),
            "image": forms.ClearableFileInput(
                attrs={"class": "form-control", "tabindex": "6"}
            ),
            "remark": forms.TextInput(attrs={"class": "form-control", "tabindex": "7"}),
            "cultivation_type": forms.Select(
                attrs={"class": "form-control", "tabindex": "8"}
            ),
            "owner": forms.Select(attrs={"class": "form-control", "tabindex": "9"}),
            "company": forms.HiddenInput(),
        }
        labels = {
            "name": "圃場名*",
            "latlon": "緯度・経度*",
            "jma_prefecture": "都道府県*",
            "jma_city": "市区町村*",
            "area": "圃場面積（㎡）",
            "image": "画像",
            "remark": "備考",
            "cultivation_type": "栽培タイプ*",
            "owner": "所有者*",
        }

    def clean_name(self):
        name = self.cleaned_data.get("name")
        company_id = self.data.get("company-id")
        if "あの" in name:
            raise forms.ValidationError(
                "「あの」を含む圃場名は登録できなくなりました（あいまい）"
            )

        if Land.objects.filter(name=name, company_id=company_id).exists():
            raise forms.ValidationError(
                "この名前の圃場は既に存在します。別の名前を選択してください"
            )

        return name


class UploadForm(forms.Form):
    file = forms.FileField(
        widget=ClearableFileInput(attrs={"class": "form-control", "tabindex": "1"})
    )

```

### base.html
```diff:soil_analysis/templates/soil_analysis/base.html
        :
    <!-- for ajax -->
    <script>let myurl = {"base": "{% url 'soil:company_list' %}"};</script>
+   <script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script>
</head>
    :
```

### create.html
```diff:soil_analysis/templates/soil_analysis/land/create.html
{% block content %}
    <div class="container">
-       <h1>圃場の作成</h1>
+       <h1>新規作成</h1>
+       {% if messages %}
+           <ul class="messages">
+               {% for message in messages %}
+                   <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
+               {% endfor %}
+           </ul>
+       {% endif %}
        <form method="post" enctype="multipart/form-data">
            {% csrf_token %}
            {{ form.as_p }}
-           <button type="submit" class="">送信</button>
+           <input type="hidden" name="company-id" value="{{ company.pk }}">
+           <button type="submit" class="btn btn-outline-primary mb-3">送信</button>
        </form>
    </div>
    
+   ここから下を追加   
    <script>
        function clearDropdown(selectElement) {
            const defaultOption = document.createElement('option')
            defaultOption.value = ""
            defaultOption.text = "選択してください"
            defaultOption.selected = true
            selectElement.appendChild(defaultOption)
        }

        function setDropdownList(optionsData, selectElement) {
            optionsData.map(item => {
                const option = document.createElement('option')
                option.value = item.id
                option.text = item.name
                selectElement.appendChild(option)
            })
        }

        /**
         * 都道府省のデータをフェッチし、それに応じて都道府県ドロップダウンオプションを設定します。
         */
        async function fetchPrefectures() {
            const prefectureSelect = document.getElementById("id_jma_prefecture")
            prefectureSelect.innerHTML = ''
            clearDropdown(prefectureSelect)

            const response = await fetch('{% url 'soil:prefectures' %}')
            const data = await response.json()
            setDropdownList(data.prefectures, prefectureSelect)
        }

        /**
         * 指定された都道府県 ID に基づいて都市データを取得し、それに応じて都市ドロップダウン オプションを設定します
         * 都道府県 ID が指定されていない場合、都市のドロップダウンはクリアされます
         *
         * @param {string} prefectureId - The ID of the prefecture.
         */
        async function fetchCities(prefectureId) {
            const citySelect = document.getElementById("id_jma_city")
            citySelect.innerHTML = ''
            clearDropdown(citySelect)

            if (prefectureId) {
                const response = await fetch(`/soil_analysis/prefecture/${prefectureId}/cities`)
                const data = await response.json()
                setDropdownList(data.cities, citySelect)
            }
        }

        /**
         * 土地の緯度経度に基づいてその位置情報を取得します
         *
         * @param {string} latlon - 緯度経度のペア。
         */
        async function fetchLandLocationInfo(latlon) {
            const response = await fetch("{% url 'soil:land_location_info' %}", {
                method: "POST",
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': Cookies.get('csrftoken')
                },
                body: JSON.stringify({latlon})
            })
            return response.json()
        }

        document.getElementById("id_latlon").addEventListener("change", async function () {
            const latlon = this.value
            const prefectureSelect = document.getElementById("id_jma_prefecture")
            const citySelect = document.getElementById("id_jma_city")

            if (latlon) {
                const landLocationInfo = await fetchLandLocationInfo(latlon)
                if (landLocationInfo.error) {
                    alert(landLocationInfo.error)
                } else {
                    await fetchPrefectures()
                    prefectureSelect.value = landLocationInfo.jma_prefecture_id
                    await fetchCities(landLocationInfo.jma_prefecture_id)
                    citySelect.value = landLocationInfo.jma_city_id
                }
            } else {
                await fetchCities(undefined)
            }
        })

        document.getElementById("id_jma_prefecture").addEventListener("change", function () {
            const prefectureId = this.value
            fetchCities(prefectureId)
        })
    </script>
+   ここまでを追加
{% endblock %}
```

### list.html
```diff:soil_analysis/templates/soil_analysis/land/list.html
                <a href="{% url 'soil:land_detail' a_land.company.pk a_land.pk %}"><h5
                        class="card-title">{{ a_land.name }}</h5></a>
                <ul class="card-text">
-                   <li>都道府県: {{ a_land.prefecture|default:"-" }}</li>
-                   <li>住所: {{ a_land.location|default:"-" }}</li>
+                   <li>都道府県: {{ a_land.jma_prefecture.name|default:"-" }}</li>
+                   <li>市区町村: {{ a_land.jma_city.name|default:"-" }}</li>
                    <li>緯度経度: {% if a_land.latlon %}<a href="https://www.google.co.jp/maps/@{{ a_land.latlon }},17z"
                                                           target="_blank">{{ a_land.latlon }}</a>{% else %}-{% endif %}
                    </li>
                    
```

### detail.html
既存をいじるからね、さすがにすべてのhtmlはのせないけど（github見てくれ）、都道府県と市区町村をyahoo-apiから取ってくるなら当然それに対応するようになおすよね？って話。

```diff:soil_analysis/templates/soil_analysis/land/detail.html
        <ul>
-           <li>都道府県: {{ object.prefecture|default:"-" }}</li>
-           <li>住所: {{ object.location|default:"-" }}</li>
+           <li>都道府県: {{ object.jma_prefecture.name|default:"-" }}</li>
+           <li>市区町村: {{ object.jma_city.name|default:"-" }}</li>
            <li>緯度経度: {{ object.latlon|default:"-" }}</li>
            <li>作型: {{ object.cultivation_type|default:"-" }}</li>
            
```

### urls.py
```diff_python:soil_analysis/urls.py
        views.LandCreateView.as_view(),
        name="land_create",
    ),
+   path("prefectures/", views.PrefecturesView.as_view(), name="prefectures"),
+   path(
+       "api/land/location/info",
+       views.LocationInfoView.as_view(),
+       name="land_location_info",
+   ),
+   path(
+       "prefecture/<int:prefecture_id>/cities",
+       views.PrefectureCitiesView.as_view(),
+       name="prefecture_cities",
+   ),
    path(
        "company/<int:company_id>/land/<int:pk>/detail",
        views.LandDetailView.as_view(),
```

### views.py
- import部分に追加しなきゃいけないのがあるけど勝手に追加してくれ（pycharmなら自動でやってくれる）

```diff_python:soil_analysis/views.py
    :
class LocationInfoView(View):
    """
    圃場新規作成時のフォームで latlon 入力が終了した際に非同期で情報を取得
    """

    @staticmethod
    def post(request, *args, **kwargs):
        data = json.loads(request.body.decode("utf-8"))
        lat_str, lon_str = data.get("latlon").split(",")
        lat = float(lat_str.strip())
        lon = float(lon_str.strip())

        coords = GoogleMapCoords(latitude=lat, longitude=lon)
        ydf = ReverseGeocoderService.get_ydf_from_coords(coords)

        try:
            jma_city = ReverseGeocoderService.get_jma_city(ydf)
        except JmaCity.DoesNotExist:
            return JsonResponse(
                {
                    "error": f"{ydf.feature.prefecture.name} {ydf.feature.city.name} が見つかりませんでした"
                }
            )

        return JsonResponse(
            {
                "jma_city_id": jma_city.id,
                "jma_prefecture_id": jma_city.jma_region.jma_prefecture.id,
            }
        )


class PrefecturesView(View):

    @staticmethod
    def get(request):
        prefectures = JmaPrefecture.objects.all()
        data = {"prefectures": list(prefectures.values("id", "name"))}
        return JsonResponse(data)


class PrefectureCitiesView(View):
    """
    圃場新規作成時のフォームで prefecture がonChangeした際に非同期で、該当するcityを取得
    """

    @staticmethod
    def get(request, prefecture_id):
        regions = JmaRegion.objects.filter(jma_prefecture__id=prefecture_id)
        cities = JmaCity.objects.filter(jma_region__in=regions)

        data = {"cities": list(cities.values("id", "name"))}
        return JsonResponse(data)
            :

```

### fixtures
```diff_python:soil_analysis/fixtures/land.json
[
  {
    "model": "soil_analysis.land",
    "fields": {
      "id": 1,
      "name": "アグリの圃場1（成田）",
      "latlon": "35.806274904252035,140.41985062979774",
      "jma_city": 600,
      "area": 100,
      "cultivation_type": 1,
      "company": 1,
      "owner": 3,
      "created_at": "2022-08-13T00:00:00+09:00",
      "remark": "備考"
    }
  },
  {
    "model": "soil_analysis.land",
    "fields": {
      "id": 2,
      "name": "アグリの圃場2（成田）",
      "latlon": "35.806304171085344,140.41816354582463",
      "jma_city": 600,
      "cultivation_type": 2,
      "company": 1,
      "owner": 3,
      "created_at": "2022-08-13T00:00:00+09:00"
    }
  },
  {
    "model": "soil_analysis.land",
    "fields": {
      "id": 3,
      "name": "アグリの圃場3（成田）",
      "latlon": "35.80640294656812,140.42094678328831",
      "jma_city": 600,
      "cultivation_type": 2,
      "company": 1,
      "owner": 4,
      "created_at": "2022-08-13T00:00:00+09:00"
    }
  },
  {
    "model": "soil_analysis.land",
    "fields": {
      "id": 4,
      "name": "農業法人2の圃場1（静岡ススムA1）",
      "latlon": "34.74415025922645,137.64878663318697",
      "jma_city": 971,
      "area": 100,
      "cultivation_type": 1,
      "company": 2,
      "owner": 3,
      "created_at": "2022-08-13T00:00:00+09:00",
      "remark": "備考"
    }
  },
  {
    "model": "soil_analysis.land",
    "fields": {
      "id": 5,
      "name": "農業法人2の圃場2（静岡ススムA2）",
      "latlon": "34.74390650637759,137.64896206679987",
      "jma_city": 971,
      "cultivation_type": 2,
      "company": 2,
      "owner": 3,
      "created_at": "2022-08-13T00:00:00+09:00"
    }
  },
  {
    "model": "soil_analysis.land",
    "fields": {
      "id": 6,
      "name": "農業法人2の圃場3（静岡ススムA3）",
      "latlon": "34.7436732368514,137.64911676734297",
      "jma_city": 971,
      "cultivation_type": 2,
      "company": 2,
      "owner": 4,
      "created_at": "2022-08-13T00:00:00+09:00"
    }
  },
  {
    "model": "soil_analysis.land",
    "fields": {
      "id": 7,
      "name": "農業法人3の圃場1（徳島）",
      "latlon": "33.93227169151002,134.50777575465415",
      "jma_city": 1422,
      "area": 100,
      "cultivation_type": 1,
      "company": 3,
      "owner": 3,
      "created_at": "2022-08-13T00:00:00+09:00",
      "remark": "備考"
    }
  },
  {
    "model": "soil_analysis.land",
    "fields": {
      "id": 8,
      "name": "農業法人3の圃場2（徳島）",
      "latlon": "33.93226507528661,134.50777734950506",
      "jma_city": 1422,
      "cultivation_type": 2,
      "company": 3,
      "owner": 3,
      "created_at": "2022-08-13T00:00:00+09:00"
    }
  },
  {
    "model": "soil_analysis.land",
    "fields": {
      "id": 9,
      "name": "農業法人3の圃場3（徳島）",
      "latlon": "33.93232726776639,134.50717130616542",
      "jma_city": 1422,
      "cultivation_type": 2,
      "company": 3,
      "owner": 4,
      "created_at": "2022-08-13T00:00:00+09:00"
    }
  }
]

```

## 圃場の住所を使って天気を表示する

### 画像を集めて static の場所に保存
バッチは　[ここ](https://qiita.com/YoshitakaOkada/items/1046fd83ef5c295491f1#create-import_weather_master_manualpy)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/e6db9310-3d34-c359-4574-d8d9fba4fe41.png)

### Landに紐づく天気コードをsvgで読む
```diff_python:soil_analysis/views.py
    :
class LandListView(ListView):
    model = Land
    template_name = "soil_analysis/land/list.html"

    def get_queryset(self):
        company = Company(pk=self.kwargs["company_id"])
+       weather_prefetch = Prefetch(
+           "jma_city__jma_region__jmaweather_set",
+           queryset=JmaWeather.objects.all(),
+           to_attr="weathers",
+       )
+       warning_prefetch = Prefetch(
+           "jma_city__jma_region__jmawarning_set",
+           queryset=JmaWarning.objects.all(),
+           to_attr="warnings",
+       )
        return (
            super()
            .get_queryset()
            .filter(company=company)
+           .prefetch_related(weather_prefetch, warning_prefetch)
        )

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        company = Company.objects.get(pk=self.kwargs["company_id"])
        land_repository = LandRepository(company)
        land_ledger_map = {
            land: land_repository.read_land_ledgers(land)
            for land in context["object_list"]
        }
        context["company"] = company
        context["land_ledger_map"] = land_ledger_map

        return context
          :  
```

```diff_html:soil_analysis/templates/soil_analysis/land/list.html
{% block content %}
    <a class="btn btn-outline-primary mb-3" href="{% url 'soil:land_create' company.id %}"
       role="button">＋圃場の追加</a>
    {% for a_land in object_list %}
        {% if not forloop.counter|divisibleby:"2" %}
            <div class="row mb-4">
        {% endif %}
    <div class="col-sm-6">
        <div class="card">
+           <div class="image-placeholder">
+               {% if a_land.image %}
+                   <img src="{{ a_land.image.url }}" alt="Land Image">
+               {% else %}
+                   <p>Now Printing</p>
+               {% endif %}
+           </div>
            <div class="card-body">
                <a href="{% url 'soil:land_detail' a_land.company.pk a_land.pk %}"><h5
                        class="card-title">{{ a_land.name }}</h5></a>
+               <div class="weather-list">
+                   {% for weather in a_land.jma_city.jma_region.weathers %}
+                       {% with 'soil_analysis/images/weather/svg/'|add:weather.jma_weather_code.summary_code|add:'.svg' as weather_svg_path %}
+                           <div class="weather-item">
+                               <img width="50" src="{% static weather_svg_path %}" alt="Weather Image">
+                               <p>{{ weather.reporting_date|date:"m/d" }}</p>
+                           </div>
+                       {% endwith %}
+                   {% empty %}
+                       <p>（該当天気なし）</p>
+                   {% endfor %}
+               </div>
+               <div class="warning-list">
+                   {% for warning in a_land.jma_city.jma_region.warnings %}
+                       <div class="warning-item">
+                           <span class="label">{{ warning.warnings }}</span>
+                       </div>
+                   {% empty %}
+                       <p>（該当警報なし）</p>
+                   {% endfor %}
+               </div>
                <ul class="card-text">
                    :
                
```

```css:soil_analysis/static/soil_analysis/css/land/list.css
    :
.weather-list {
    display: flex;
    flex-wrap: wrap;
}

.weather-item {
    margin: 10px;
}

.weather-item p {
    text-align: center;
}
```

## 確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/1f6a1a41-8034-287a-337e-bcfe1ed8702e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/9ff08833-f0f1-c3c1-9f53-97d529de6f4a.png)