import sys
import os
import glob
import re
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLabel, QComboBox,
QTextEdit, QMessageBox)
from PyQt6.QtCore import Qt
# ==========================================
# 1. 設定・地点データ定義
# ==========================================
CACHE_DIR = “gpv_cache_npz”
LOCATIONS = [
(“中央区土木センター”, 43.0715, 141.3184), (“北区土木センター”, 43.1257, 141.3444),
(“東区土木センター”, 43.1065, 141.3732), (“白石区土木センター”, 43.0425, 141.4239),
(“厚別区土木センター”, 43.0298, 141.4682), (“豊平区土木センター”, 43.0229, 141.4012),
(“南区土木センター”, 43.0134, 141.3397), (“西区土木センター”, 43.0763, 141.2722),
(“手稲区土木センター”, 43.1118, 141.2559), (“南16条西4丁目道路用地”, 43.0401, 141.3535),
(“円山公園坂下野球場”, 43.0526, 141.3148), (“盤渓配水池”, 43.0315, 141.2861),
(“北26条通り西8丁目”, 43.0905, 141.3417), (“新琴似グリーン公園”, 43.1165, 141.3211),
(“拓北水再生プラザ”, 43.1585, 141.3852), (“茨戸東部中継ポンプ場”, 43.1561, 141.3725),
(“農業支援センター”, 43.1154, 141.4429), (“札苗公園”, 43.1054, 141.4137),
(“一条大橋橋台広場”, 43.0592, 141.3653), (“豊平川水再生プラザ”, 43.0560, 141.3831),
(“厚別水再生プラザ”, 43.0255, 141.4617), (“大谷地流通団地東緑地”, 43.0238, 141.4552),
(“平岡公園”, 43.0062, 141.4632), (“清田区土木センター”, 43.0084, 141.4398),
(“西岡公園”, 42.9961, 141.3934), (“里塚霊園”, 42.9818, 141.4568),
(“中の沢雨水調整池”, 42.9933, 141.3175), (“石山1条4丁目緑地”, 42.9649, 141.3409),
(“藤野公園”, 42.9550, 141.3228), (“定山渓水再生プラザ”, 42.9734, 141.1718),
(“滝野自然学園”, 42.9161, 141.3957), (“福井中央公園”, 43.0416, 141.2676),
(“平和霊園”, 43.0317, 141.2585), (“手稲水再生プラザ”, 43.1257, 141.2335),
(“花川汚水中継ポンプ場”, 43.1678, 141.3155), (“高岡小中学校跡地”, 43.2081, 141.4239),
(“石狩斎場”, 43.2185, 141.3204), (“茨戸水再生プラザ”, 43.1612, 141.3456),
(“太美町汚水処理センター”, 43.1764, 141.4468), (“当別町下水終末処理場”, 43.2182, 141.5178),
(“金沢会館”, 43.2505, 141.5645), (“榎本公園”, 43.1091, 141.5312),
(“上江別南町公園”, 43.0963, 141.5547), (“大麻東公園”, 43.0805, 141.5037),
(“野幌農村環境改善センター”, 43.0645, 141.5204), (“北広島市白樺プール”, 42.9732, 141.5658),
(“島松寿町会館”, 42.9026, 141.5724), (“西部小学校”, 42.8557, 141.5303),
(“西の里”, 43.0232, 141.5199), (“東8丁目アンダーパス”, 43.0682, 141.3595),
(“苗穂アンダーパス”, 43.0684, 141.3725), (“篠路アンダーパス”, 43.1361, 141.3533),
(“百合が原アンダーパス”, 43.1205, 141.3644), (“新川アンダーパス”, 43.0988, 141.3235),
(“菊水アンダーパス”, 43.0618, 141.3776), (“もみじ台通アンダーパス”, 43.0245, 141.4831),
(“上野幌アンダーパス”, 43.0185, 141.4842), (“創成トンネル”, 43.0601, 141.3551),
(“エルムトンネル”, 43.0768, 141.3392), (“平岡跨道橋”, 43.0165, 141.4589),
(“大谷地跨道橋”, 43.0242, 141.4591)
]
loc_names = [loc[0] for loc in LOCATIONS]
# ==========================================
# 2. 汎用抽出ロジック
# ==========================================
def fuzzy_key_search(data_obj, keywords, exclude=None):
if exclude is None: exclude = [‘lon’, ‘lat’, ‘time’]
for k in data_obj.files:
kl = k.lower()
if any(ex in kl for ex in exclude): continue
if any(kw in kl for kw in keywords): return k
return None
def get_coords_for_data(data_obj, data_array):
ny = data_array.shape[0]; nx = data_array.shape[1] if data_array.ndim > 1 else len(data_array)
for k in data_obj.files:
if ‘lon’ in k.lower():
if (data_obj[k].ndim == 1 and data_obj[k].shape[0] == nx) or (data_obj[k].ndim == 2 and data_obj[k].shape == data_array.shape):
lat_k = k.replace(‘lon’, ‘lat’).replace(‘Lon’, ‘Lat’)
if lat_k in data_obj.files: return data_obj[k], data_obj[lat_k]
return None, None
def get_nearest_index(lon_arr, lat_arr, target_lon, target_lat):
if lon_arr.ndim == 1:
lon_2d, lat_2d = np.meshgrid(lon_arr, lat_arr)
else:
lon_2d, lat_2d = lon_arr, lat_arr
dist = (lon_2d – target_lon)**2 + (lat_2d – target_lat)**2
return np.unravel_index(np.argmin(dist), dist.shape)
def calc_wind_direction(u, v):
if u == 0 and v == 0: return “静穏”
deg = (270 – np.degrees(np.arctan2(v, u))) % 360
dirs = [‘北’, ‘北北東’, ‘北東’, ‘東北東’, ‘東’, ‘東南東’, ‘南東’, ‘南南東’,
‘南’, ‘南南西’, ‘南西’, ‘西南西’, ‘西’, ‘西北西’, ‘北西’, ‘北北西’]
idx = int((deg + 11.25) / 22.5) % 16
return dirs[idx]
# ==========================================
# 3. GUI アプリケーション本体
# ==========================================
class ExcelGeneratorApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle(“GPV局地予報 Excel出力ツール”)
self.resize(700, 500)
self.setStyleSheet(“””
QWidget { background-color: #0A192F; color: #E0E0E0; font-family: “MS Gothic”, sans-serif; font-size: 11pt; }
QPushButton { background-color: #1D3557; border: 1px solid #457B9D; padding: 10px; border-radius: 5px; font-weight: bold; }
QPushButton:hover { background-color: #457B9D; }
.CreateBtn { background-color: #27AE60; color: white; font-size: 12pt;}
.CreateBtn:hover { background-color: #2ECC71; }
.StopBtn { background-color: #C0392B; color: white; }
.StopBtn:hover { background-color: #E74C3C; }
QComboBox { background-color: #112240; border: 1px solid #64FFDA; padding: 5px; color: white;}
QTextEdit { background-color: #112240; border: 1px solid #444; color: #64FFDA; font-family: Consolas;}
“””)
main_widget = QWidget()
self.setCentralWidget(main_widget)
layout = QVBoxLayout(main_widget)
# ————————-
# コントロールパネル
# ————————-
ctrl_layout = QHBoxLayout()
# モデル選択
ctrl_layout.addWidget(QLabel(“モデル:”))
self.model_combo = QComboBox()
self.model_combo.addItems([“MSM”, “GSM (日本域)”])
self.model_combo.currentIndexChanged.connect(self.refresh_init_times)
ctrl_layout.addWidget(self.model_combo)
# 初期時刻選択
ctrl_layout.addWidget(QLabel(“初期時刻:”))
self.init_time_combo = QComboBox()
self.init_time_combo.setMinimumWidth(200)
ctrl_layout.addWidget(self.init_time_combo)
# 更新ボタン
refresh_btn = QPushButton(“🔄 初期時更新”)
refresh_btn.clicked.connect(self.refresh_init_times)
ctrl_layout.addWidget(refresh_btn)
layout.addLayout(ctrl_layout)
# ————————-
# 実行・ログパネル
# ————————-
btn_layout = QHBoxLayout()
self.create_btn = QPushButton(“📊 Excelファイル作成”)
self.create_btn.setProperty(“class”, “CreateBtn”)
self.create_btn.clicked.connect(self.run_extraction)
exit_btn = QPushButton(“終了”)
exit_btn.setProperty(“class”, “StopBtn”)
exit_btn.clicked.connect(self.close)
btn_layout.addWidget(self.create_btn)
btn_layout.addWidget(exit_btn)
layout.addLayout(btn_layout)
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
layout.addWidget(self.log_output)
# 起動時に一度フォルダをスキャン
self.refresh_init_times()
def log(self, message):
“””ログをテキストエリアに出力”””
self.log_output.append(message)
# GUIがフリーズしないよう強制描画
QApplication.processEvents()
def refresh_init_times(self):
“””キャッシュディレクトリから選択中モデルの初期時刻を取得してコンボボックスにセット”””
self.init_time_combo.clear()
if not os.path.exists(CACHE_DIR):
self.log(f”エラー: ディレクトリ ‘{CACHE_DIR}’ が見つかりません。”)
return
model_prefix = “MSM” if self.model_combo.currentIndex() == 0 else “GSM_JP”
# ファイルリスト取得
files = glob.glob(os.path.join(CACHE_DIR, f”{model_prefix}_*.npz”))
if not files:
self.init_time_combo.addItem(“データなし”, None)
return
# 初期時刻文字列(YYYYMMDDHHMMSS)を抽出してユニークに
times = sorted(list(set(re.search(r’_(\d{14})_’, f).group(1) for f in files if re.search(r’_(\d{14})_’, f))), reverse=True)
for t in times:
dt = datetime.strptime(t, “%Y%m%d%H%M%S”) + timedelta(hours=9)
display_str = dt.strftime(‘%Y/%m/%d %H:00 (JST)’)
if t == times[0]: display_str += ” [最新]”
self.init_time_combo.addItem(display_str, t)
self.log(f”{model_prefix}の初期時刻リストを更新しました。”)
def run_extraction(self):
“””Excel作成のメイン処理”””
init_time_str = self.init_time_combo.currentData()
if not init_time_str:
QMessageBox.warning(self, “エラー”, “有効な初期時刻が選択されていません。”)
return
model_name = “MSM” if self.model_combo.currentIndex() == 0 else “GSM”
model_prefix = “MSM” if model_name == “MSM” else “GSM_JP”
output_excel = f”{model_name}_Sapporo_Local_Forecast.xlsx”
self.log(f”\n======================================”)
self.log(f”🚀 {model_name} Excel作成処理を開始します…”)
self.log(f”対象初期時刻: {self.init_time_combo.currentText()}”)
files = glob.glob(os.path.join(CACHE_DIR, f”{model_prefix}_{init_time_str}_FT*.npz”))
fts = sorted([int(re.search(r’_FT(\d+)\.npz’, f).group(1)) for f in files])
if not fts:
self.log(“エラー: FTファイルが見つかりません。”)
return
init_dt = datetime.strptime(init_time_str, “%Y%m%d%H%M%S”) + timedelta(hours=9)
time_headers = [(init_dt + timedelta(hours=ft)).strftime(“%m/%d %H:00”) for ft in fts]
# 格納用DF
df_temp = pd.DataFrame(index=loc_names, columns=time_headers)
df_wdir = pd.DataFrame(index=loc_names, columns=time_headers)
df_wspd = pd.DataFrame(index=loc_names, columns=time_headers)
df_precip = pd.DataFrame(index=loc_names, columns=time_headers)
df_snow = pd.DataFrame(index=loc_names, columns=time_headers)
df_cloud = pd.DataFrame(index=loc_names, columns=time_headers)
self.create_btn.setEnabled(False)
for ft, t_head in zip(fts, time_headers):
file_path = os.path.join(CACHE_DIR, f”{model_prefix}_{init_time_str}_FT{ft:02d}.npz”)
try:
data = np.load(file_path)
k_t2m = fuzzy_key_search(data, [‘t2m’, ‘tmp2m’, ‘temp2m’])
k_u10 = fuzzy_key_search(data, [‘u10′, ’10u’, ‘u_10m’])
k_v10 = fuzzy_key_search(data, [‘v10′, ’10v’, ‘v_10m’])
k_pr = fuzzy_key_search(data, [‘apcp’, ‘pr’, ‘precip’, ‘rain’, ‘tp’])
k_sn = fuzzy_key_search(data, [‘snow’, ‘snwe’, ‘csnow’, ‘pos’])
k_cc = fuzzy_key_search(data, [‘tcc’, ‘tcdc’, ‘lcdc’, ‘cloud’])
if k_t2m:
lon_arr, lat_arr = get_coords_for_data(data, data[k_t2m])
else:
self.log(f”FT={ft:02d}: 気温データがないためスキップします。”)
continue
for loc_name, lat, lon in LOCATIONS:
sy, sx = get_nearest_index(lon_arr, lat_arr, lon, lat)
if k_t2m:
t_val = float(data[k_t2m][sy, sx])
if t_val > 100: t_val -= 273.15
df_temp.at[loc_name, t_head] = round(t_val, 1)
if k_u10 and k_v10:
u_val = float(data[k_u10][sy, sx])
v_val = float(data[k_v10][sy, sx])
wspd = np.hypot(u_val, v_val)
wdir = calc_wind_direction(u_val, v_val)
df_wspd.at[loc_name, t_head] = round(wspd, 1)
df_wdir.at[loc_name, t_head] = wdir
if k_pr:
pr_val = float(data[k_pr][sy, sx])
df_precip.at[loc_name, t_head] = round(max(pr_val, 0.0), 1)
if k_sn:
sn_val = float(data[k_sn][sy, sx])
df_snow.at[loc_name, t_head] = round(max(sn_val, 0.0), 1)
else:
df_snow.at[loc_name, t_head] = np.nan
if k_cc:
cc_val = float(data[k_cc][sy, sx])
if cc_val <= 1.0 and cc_val > 0: cc_val *= 100
df_cloud.at[loc_name, t_head] = round(cc_val, 0)
data.close()
self.log(f”FT={ft:02d} のデータ抽出完了”)
except Exception as e:
self.log(f”FT={ft:02d} の読み込みエラー: {e}”)
# Excel出力 (C3基準レイアウト)
self.log(f”Excelファイル ‘{output_excel}’ に出力中…”)
try:
with pd.ExcelWriter(output_excel, engine=’openpyxl’) as writer:
df_temp.to_excel(writer, sheet_name=”気温”, startrow=1, startcol=1, index_label=”地点名”)
df_wdir.to_excel(writer, sheet_name=”風向”, startrow=1, startcol=1, index_label=”地点名”)
df_wspd.to_excel(writer, sheet_name=”風速”, startrow=1, startcol=1, index_label=”地点名”)
df_precip.to_excel(writer, sheet_name=”降水量”, startrow=1, startcol=1, index_label=”地点名”)
df_snow.to_excel(writer, sheet_name=”降雪量”, startrow=1, startcol=1, index_label=”地点名”)
df_cloud.to_excel(writer, sheet_name=”雲量”, startrow=1, startcol=1, index_label=”地点名”)
self.log(f”✅ 保存完了: {os.path.abspath(output_excel)}”)
QMessageBox.information(self, “処理完了”, f”Excelファイルを作成しました!\n{output_excel}”)
except Exception as e:
self.log(f”Excel保存エラー: {e}”)
QMessageBox.critical(self, “保存エラー”, f”Excelの保存に失敗しました。\n{e}”)
self.create_btn.setEnabled(True)
if __name__ == “__main__”:
app = QApplication(sys.argv)
window = ExcelGeneratorApp()
window.show()
sys.exit(app.exec())

コメント