はじめに
この記事で気象庁データを溜め込むまでを書いた。この記事はそれの活用である
適用するのはおなじみ、ポートフォリオ(soil_analysis)だ
改修の方針
このアプリは、圃場(畑とか田んぼとかを抽象化した名前)の場所を取り扱うので天気予報にぴったりだ。いまは住所を手入れしているがまぁ圃場の場所だからな、個人でいくつかあるわけだしいちいち住所をいれるのは面倒だろうから、エンハンス(=改修)として
- 圃場の登録時・編集時に、緯度経度だけの入力とする
- 緯度経度から住所を特定する(市町村レベルまででよいので無料のツールを利用する)
- 圃場の住所を使って天気を表示する
Yahoo!リバースジオコーダAPI
緯度経度から住所を取得するものを逆ジオコーディングという(住所から緯度経度を取得するものをジオコーディングという)
登録から使用の手順
まあgcp使うときもアプリケーションの登録はするわけだし、ええやろ
- Yahoo! JAPAN IDを取得
- アプリケーションを登録
- ID連携利用有無: ID連携を利用しない(YOLPを使用するだけ)
- 利用者情報: 個人
- 個人情報提供先としてユーザーへ開示することに同意しますか?: 同意しない(見た感じ大丈夫そう なので、まぁこれでやってみよう)
- アプリケーション名: soil analysis
- 登録完了!
- APIドキュメントを読む
- アプリケーションを作成する
- アプリケーションを公開する
手に入れたAPIキーを.envにまとめる
.env.example
:
YAHOO_CLIENT_ID=
.env
:
YAHOO_CLIENT_ID=****
緯度経度から住所を特定する
リクエストURL
緯度経度の入力から住所を取得
都道府県、市区町村、残り住所 に住所をセパレートできる
vo
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()
- 緯度経度の情報を持って yahoo reverseGeoCoder にアクセス
- 気象庁のcityレコードを取得する
get_jma_city()
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
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>東京都港区赤坂9丁目7-1</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>9丁目</Name>
<Kana>9ちょうめ</Kana>
<Level>aza</Level>
</AddressElement>
<AddressElement>
<Name>7</Name>
<Kana>7</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 == "東京都港区赤坂9丁目7-1"
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 == "赤坂9丁目7"
assert ydf.feature.detail.kana == "あかさか9ちょうめ7"
assert ydf.feature.detail.level == "detail"
圃場の登録時に、緯度経度だけの入力とする
(更新するページはいまのところ作っていないので省略)
models.py
- もとのコードでjmaシリーズのモデルがLandのモデルの下にあったので上に持ってきたけど、その差分はさすがに書いていない
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から住所もってきてその場で確認(取得できなかったらドロップダウンが出てくるみたいな)というのもつけたら便利そうだけど
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
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
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
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から取ってくるなら当然それに対応するようになおすよね?って話。
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
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なら自動でやってくれる)
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
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 の場所に保存
バッチは ここ
Landに紐づく天気コードをsvgで読む
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
:
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">
:
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;
}