未分類

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())

コメント