未分類

import sys
import os
import gzip
import requests
import pandas as pd
import xml.etree.ElementTree as ET

from PyQt6.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QFileDialog,
QPushButton,
QLabel,
QListWidget,
QVBoxLayout,
QHBoxLayout,
QComboBox,
QLineEdit,
QMessageBox
)

from PyQt6.QtCore import Qt

class MainWindow(QMainWindow):
def __init__(self):
super().__init__()

self.setWindowTitle("GSM地点ガイダンスビューア")
self.resize(1400, 900)

self.station_dict = {}
self.station_names = {}

self.gsm_data = {}
self.msm_data = {}
self.humidity_codes = set()

self.init_ui()

self.load_station_master()

self.gsm_file_list.currentRowChanged.connect(
self.load_selected_gsm_file
)
self.msm_file_list.currentRowChanged.connect(
self.load_selected_msm_file
)

self.station_combo.currentTextChanged.connect(
self.station_changed
)

# --------------------------------------------------
# UI
# --------------------------------------------------
def init_ui(self):

from PyQt6.QtWidgets import QTableWidget

central = QWidget()
self.setCentralWidget(central)

layout = QVBoxLayout()
central.setLayout(layout)

# ==================================
# 上段
# 地点選択 + Excel出力
# ==================================

top_layout = QHBoxLayout()
top_layout.addWidget(
QLabel("地点")
)
self.station_search = QLineEdit()

self.station_search.setPlaceholderText(
"地点検索"
)
top_layout.addWidget(
self.station_search
)
self.station_combo = QComboBox()

top_layout.addWidget(
self.station_combo
)
self.btn_excel = QPushButton(
"Excel出力"
)
top_layout.addWidget(
self.btn_excel
)
layout.addLayout(
top_layout
)

# ==================================
# GSM / MSM フォルダ
# ==================================

folder_layout = QHBoxLayout()

# ----------------------------------
# GSM
# ----------------------------------
gsm_layout = QVBoxLayout()
gsm_layout.addWidget(
QLabel("GSM")
)
self.btn_gsm_folder = QPushButton(
"GSMフォルダ選択"
)

self.gsm_folder_label = QLabel(
"GSMフォルダ未選択"
)
self.gsm_file_list = QListWidget()
gsm_layout.addWidget(
self.btn_gsm_folder
)
gsm_layout.addWidget(
self.gsm_folder_label
)

gsm_layout.addWidget(
self.gsm_file_list
)

# ----------------------------------
# MSM
# ----------------------------------
msm_layout = QVBoxLayout()
msm_layout.addWidget(
QLabel("MSM")
)
self.btn_msm_folder = QPushButton(
"MSMフォルダ選択"
)
self.msm_folder_label = QLabel(
"MSMフォルダ未選択"
)
self.msm_file_list = QListWidget()
msm_layout.addWidget(
self.btn_msm_folder
)
msm_layout.addWidget(
self.msm_folder_label
)
msm_layout.addWidget(
self.msm_file_list
)
folder_layout.addLayout(
gsm_layout
)
folder_layout.addLayout(
msm_layout
)
layout.addLayout(
folder_layout
)
# ==================================
# GSMテーブル
# ==================================
layout.addWidget(
QLabel("GSM ガイダンス")
)
self.gsm_table = QTableWidget()
layout.addWidget(
self.gsm_table
)

# ==================================
# MSMテーブル
# ==================================

layout.addWidget(
QLabel("MSM ガイダンス")
)
self.msm_table = QTableWidget()
layout.addWidget(
self.msm_table
)

# ==================================
# シグナル
# ==================================

self.btn_gsm_folder.clicked.connect(
self.select_gsm_folder
)

self.btn_msm_folder.clicked.connect(
self.select_msm_folder
)
self.station_search.textChanged.connect(
self.filter_station
)

# --------------------------------------------------
# 地点マスタ
# --------------------------------------------------

def load_station_master(self):
try:
url = (
"https://www.jma.go.jp/"
"bosai/amedas/const/amedastable.json"
)
r = requests.get(url, timeout=20)
data = r.json()
df = pd.DataFrame.from_dict(
data,
orient="index"
)
self.station_dict = {
str(idx): row["kjName"]
for idx, row in df.iterrows()
}
self.station_names = {
row["kjName"]: str(idx)
for idx, row in df.iterrows()
}
self.station_combo.clear()
names = sorted(self.station_names.keys())
self.refresh_station_combo()
print(
f"地点数: {len(self.station_names)}"
)

except Exception as e:

QMessageBox.warning(
self,
"地点マスタ取得失敗",
str(e)
)

# --------------------------------------------------
# 検索
# --------------------------------------------------

def filter_station(self):
self.refresh_station_combo()

# --------------------------------------------------
# GSMフォルダ選択
# --------------------------------------------------

def select_gsm_folder(self):

folder = QFileDialog.getExistingDirectory(
self,
"GSMフォルダ選択"
)
if not folder:
return
self.gsm_folder_label.setText(
folder
)

self.load_gsm_files(folder)

# --------------------------------------------------
# MSMフォルダ選択
# --------------------------------------------------

def select_msm_folder(self):

folder = QFileDialog.getExistingDirectory(
self,
"MSMフォルダ選択"
)
if not folder:
return
self.msm_folder_label.setText(
folder
)

self.load_msm_files(folder)

# --------------------------------------------------
# GSMファイル検索
# --------------------------------------------------

def load_gsm_files(self, folder):
self.gsm_file_list.clear()

files = []
for file in os.listdir(folder):
if (
file.endswith(".xml")
or
file.endswith(".xml.gz")
):

files.append(file)
files.sort(reverse=True)

for file in files:

display = self.build_display_name(
file
)
self.gsm_file_list.addItem(
display
)

# --------------------------------------------------
# MSMファイル検索
# --------------------------------------------------

def load_msm_files(self, folder):
self.msm_file_list.clear()
files = []
for file in os.listdir(folder):
if (
file.endswith(".xml")
or
file.endswith(".xml.gz")
):
files.append(file)

files.sort(reverse=True)
for file in files:

display = self.build_display_name(
file
)
self.msm_file_list.addItem(
display
)

# --------------------------------------------------
# 表示名作成
# --------------------------------------------------

def build_display_name(self, filename):

try:
parts = filename.split("_")
dt = parts[3]
y = dt[0:4]
m = dt[4:6]
d = dt[6:8]
h = dt[8:10]

return (
f"{y}/{m}/{d} {h}UTC "
f"{filename}"
)
except:

return filename
def load_selected_gsm_file(self):
item = self.gsm_file_list.currentItem()
if item is None:
return
filename = item.text()
pos = filename.find("UTC")
if pos >= 0:
filename = filename[pos + 7:]
folder = self.gsm_folder_label.text()
filepath = os.path.join(
folder,
filename
)
self.parse_xml(
filepath,
"GSM"
)
def load_selected_msm_file(self):

item = self.msm_file_list.currentItem()
if item is None:
return
filename = item.text()
pos = filename.find("UTC")
if pos >= 0:
filename = filename[pos + 7:]

folder = self.msm_folder_label.text()

filepath = os.path.join(
folder,
filename
)

self.parse_xml(
filepath,
"MSM"
)

def refresh_station_combo(self):
current = self.station_search.text().strip()
self.station_combo.clear()
for name in sorted(self.station_names.keys()):

if current:

if current not in name:
continue
code = self.station_names[name]
display = f"{name} [{code}]"
self.station_combo.addItem(
display
)
def get_initial_hour(self, filepath):
try:
filename = os.path.basename(filepath)
parts = filename.split("_")
dtstr = parts[3]
return int(dtstr[8:10])
except:
return 0

def parse_xml(self, filepath,model):
try:
tree = ET.parse(filepath)
root = tree.getroot()
if model == "GSM":
self.gsm_data = {}
target = self.gsm_data
else:

self.msm_data = {}

target = self.msm_data

self.humidity_codes = set()

for tsi in root.findall(".//{*}TimeSeriesInfo"):
time_map = {}
for td in tsi.findall(".//{*}TimeDefine"):
tid = td.attrib.get("timeId")
dt = td.findtext(".//{*}DateTime")
if tid and dt:
time_map[tid] = dt
for item in tsi.findall(".//{*}Item"):

type_name = item.findtext(
".//{*}Type"
)

station = item.find(".//{*}Station")

if station is None:
continue
code = station.findtext(".//{*}Code")
if code is None:
continue
kind = item.find(".//{*}Kind")
if kind is None:
continue
prop = kind.find(".//{*}Property")
if prop is None:
continue
type_name = prop.findtext(
".//{*}Type"
)
if code not in target:
target[code] = {}
# --------------------------------
# 気温
# --------------------------------

if type_name == "気温":
target[code]["気温"] = []
for temp in prop.findall(".//{*}Temperature"):

refid = temp.attrib.get("refId")
dt = time_map.get(refid)

target[code]["気温"].append(
(dt, temp.text)
)
# --------------------------------
# 風
# --------------------------------
elif type_name == "風":
target[code]["風向"] = []

for elem in prop.findall(
".//{*}WindDirection"
):
refid = elem.attrib.get("refId")
dt = time_map.get(refid)
target[code]["風向"].append(
(dt, elem.text)
)
target[code]["風速"] = []
for elem in prop.findall(
".//{*}WindSpeed"
):
refid = elem.attrib.get("refId")
dt = time_map.get(refid)
target[code]["風速"].append(
(dt, elem.text)
)
# --------------------------------
# 最大風
# --------------------------------

elif type_name == "最大風":
target[code]["最大風向"] = []
for elem in prop.findall(
".//{*}WindDirection"
):
refid = elem.attrib.get("refId")
dt = time_map.get(refid)
target[code]["最大風向"].append(
(dt, elem.text)
)
target[code]["最大風速"] = []
for elem in prop.findall(
".//{*}WindSpeed"
):
refid = elem.attrib.get("refId")
dt = time_map.get(refid)
target[code]["最大風速"].append(
(dt, elem.text)
)
# --------------------------------
# 朝の最低気温
# --------------------------------
elif type_name == "朝の最低気温":
target[code]["朝の最低気温"] = []
for elem in prop.findall(
".//{*}Temperature"
):

refid = elem.attrib.get("refId")
dt = time_map.get(refid)
target[code]["朝の最低気温"].append(
(dt, elem.text)
)
# --------------------------------
# 日中の最高気温
# --------------------------------

elif type_name == "日中の最高気温":
target[code]["日中の最高気温"] = []
for elem in prop.findall(
".//{*}Temperature"
):
refid = elem.attrib.get("refId")
dt = time_map.get(refid)
target[code]["日中の最高気温"].append(
(dt, elem.text)
)

# --------------------------------
# 最小湿度
# --------------------------------
elif type_name == "最小湿度":
self.humidity_codes.add(code)
if code not in target:
target[code] = {}
target[code]["最小湿度"] = []
for elem in item.findall(".//{*}Humidity"):
refid = elem.attrib.get("refId")
dt = time_map.get(refid)
target[code]["最小湿度"].append(
(dt, elem.text)
)

if model == "GSM":
self.gsm_initial_hour = self.get_initial_hour(
filepath
)

else:
self.msm_initial_hour = self.get_initial_hour(
filepath
)

self.station_changed()

except Exception as e:
QMessageBox.warning(self, "読込失敗", str(e))

def station_changed(self):
name = self.station_combo.currentText()
if "[" in name:
name = name.split("[")[0].strip()
code = self.station_names.get(name)
if code is None:
return

if code in self.gsm_data:

self.fill_table(
self.gsm_table,
self.gsm_data,
code
)

if code in self.msm_data:

self.fill_table(
self.msm_table,
self.msm_data,
code
)

def fill_table(
self,
table,
data,
code
):

from PyQt6.QtWidgets import QTableWidgetItem

elements = [
"気温",
"風向",
"風速",
"最大風向",
"最大風速",
"朝の最低気温",
"日中の最高気温",
"最小湿度"
]

temps = data[code].get(
"気温",
[]
)

if len(temps) == 0:
return
table.clear()
table.setRowCount(
len(elements)
)
table.setColumnCount(
len(temps)
)

headers = []

jst_hours = []

for dt, value in temps:

jst = (
pd.to_datetime(
dt,
utc=True
).tz_convert(
"Asia/Tokyo"
)
)

headers.append(
jst.strftime("%m/%d\n%H")
)

jst_hours.append(
jst.hour
)

table.setHorizontalHeaderLabels(
headers
)

table.setVerticalHeaderLabels(
elements
)

for row, element in enumerate(elements):

values = data[code].get(
element,
[]
)

if len(values) == 0:

table.setItem(
row,
0,
QTableWidgetItem("-")
)

continue

# --------------------------------
# 風系
# --------------------------------

if element in [
"風向",
"風速",
"最大風向",
"最大風速"
]:

for dt, value in values:

hour = (
pd.to_datetime(
dt,
utc=True
)
.tz_convert(
"Asia/Tokyo"
)
.hour
)

try:
col = jst_hours.index(hour)
except:
continue

table.setItem(
row,
col,
QTableWidgetItem(
str(value)
)
)
continue

# --------------------------------
# 最高気温
# --------------------------------
if element == "日中の最高気温":
idx = 0
for col, hour in enumerate(jst_hours):
if hour == 15:
if idx < len(values):
table.setItem(
row,
col,
QTableWidgetItem(
str(values[idx][1])
)
)
idx += 1
continue

# --------------------------------
# 最低気温
# --------------------------------

if element == "朝の最低気温":
idx = 0
for col, hour in enumerate(jst_hours):
if hour == 3:
if idx < len(values):
table.setItem(
row,
col,
QTableWidgetItem(
str(values[idx][1])
)
)
idx += 1
continue
# --------------------------------
# 気温・湿度
# --------------------------------

for col, (dt, value) in enumerate(values):
table.setItem(
row,
col,
QTableWidgetItem(
str(value)
)
)

table.resizeColumnsToContents()
table.resizeRowsToContents()


if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())