未分類

# ==========================================

# 統合気象データ分析・予報出力プラットフォーム

# VERSION INFO: 1.00 (3タブ構成・CSV地点読込・SQLite/Excel完全連携版)

# ==========================================

import sys, os, glob, re, time, json, sqlite3, csv

import subprocess

import multiprocessing

from datetime import datetime, timedelta

import numpy as np

from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,

                             QPushButton, QLabel, QListWidget, QFileDialog, QMessageBox,

                             QProgressBar, QTabWidget, QTableWidget, QTableWidgetItem,

                             QHeaderView, QComboBox, QDateTimeEdit, QLineEdit)

from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QSettings, QDateTime

# Matplotlibの埋め込み用

import matplotlib

matplotlib.use(‘qtagg’)

import matplotlib.pyplot as plt

import matplotlib.dates as mdates

from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas

from matplotlib.figure import Figure

DB_FILE = “sapporo_1km_mesh.db”

CSV_FILE = “forecast_points.csv”

# —————————————–

# CSVの初期雛形作成(30地点分)

# —————————————–

def create_default_points_csv():

    if os.path.exists(CSV_FILE): return

    default_points = [

        (“札幌(中央区)”, 43.060, 141.328), (“北区”, 43.091, 141.341), (“東区”, 43.075, 141.369),

        (“白石区”, 43.037, 141.414), (“厚別区”, 43.030, 141.474), (“豊平区”, 43.031, 141.379),

        (“清田区”, 42.999, 141.442), (“南区”, 42.991, 141.353), (“西区”, 43.076, 141.300),

        (“手稲区”, 43.120, 141.244), (“手稲山”, 43.076, 141.196), (“札幌ドーム”, 43.015, 141.410),

        (“大通公園”, 43.060, 141.347), (“すすきの”, 43.055, 141.354), (“定山渓温泉”, 42.965, 141.164),

        (“新千歳空港”, 42.775, 141.692), (“江別”, 43.106, 141.537), (“千歳”, 42.823, 141.652),

        (“恵庭”, 42.883, 141.583), (“北広島”, 42.983, 141.563), (“石狩”, 43.166, 141.354),

        (“当別”, 43.218, 141.514), (“小樽”, 43.191, 141.002), (“岩見沢”, 43.204, 141.768),

        (“苫小牧”, 42.634, 141.605), (“あいの里”, 43.155, 141.401), (“真駒内”, 43.007, 141.357),

        (“百合が原”, 43.131, 141.371), (“羊ヶ丘”, 43.006, 141.412), (“宮の沢”, 43.089, 141.278)

    ]

    with open(CSV_FILE, mode=’w’, encoding=’utf-8′, newline=”) as f:

        writer = csv.writer(f)

        writer.writerow([“地点名”, “緯度”, “経度”])

        for p in default_points:

            writer.writerow(p)

# —————————————–

# DB自動監視・登録スレッド(App C ロボットの移植)

# —————————————–

class DBAutoLoaderWorker(QThread):

    log_signal = pyqtSignal(str)

    progress_signal = pyqtSignal(int, int)

    def __init__(self, npz_dir):

        super().__init__()

        self.npz_dir = npz_dir

        self.is_running = True

    def run(self):

        self.log_signal.emit(“🏭 DB自動監視スレッドが稼働しました。”)

        processed_files = set()

        while self.is_running:

            try:

                npz_files = glob.glob(os.path.join(self.npz_dir, “*.npz”))

                new_files = [f for f in npz_files if f not in processed_files]

                if new_files:

                    conn = sqlite3.connect(DB_FILE)

                    c = conn.cursor()

                    self.log_signal.emit(f”📦 新規ファイル {len(new_files)}件を検出。解析中…”)

                    for idx, fpath in enumerate(new_files):

                        if not self.is_running: break

                        fname = os.path.basename(fpath)

                        m = re.match(r'(MSM|GSM_JP|MSM_GUID|GSM_GUID)_(\d{14})_FT(\d+)\.npz’, fname)

                        if not m:

                            processed_files.add(fpath)

                            continue

                        model, it_str, ft_str = m.groups()

                        ft = int(ft_str)

                        init_dt = datetime.strptime(it_str, “%Y%m%d%H%M%S”) # 内部時間:完全UTC

                        valid_dt = init_dt + timedelta(hours=ft)

                        # ダミーデータ挿入(プロトタイプ用の高速モック)

                        # 実際の運用時はここへ前回の1km切り出し・最近傍抽出ロジックを流し込みます

                        c.execute(”’

                            INSERT OR REPLACE INTO weather_data

                            (model, init_time, valid_time, forecast_time, lat, lon, precip, temp)

                            VALUES (?, ?, ?, ?, ?, ?, ?, ?)

                        ”’, (model, init_dt.isoformat(), valid_dt.isoformat(), ft, 43.06, 141.35, 0.0, 15.5))

                        processed_files.add(fpath)

                        self.progress_signal.emit(idx + 1, len(new_files))

                    conn.commit()

                    conn.close()

                    self.log_signal.emit(f”✅ バッチ処理完了。{len(new_files)}件をDB同期しました。”)

            except Exception as e:

                self.log_signal.emit(f”⚠️ 監視エラー: {e}”)

            for _ in range(5):

                if not self.is_running: break

                self.msleep(1000)

    def stop(self):

        self.is_running = False

# —————————————–

# メインウィンドウ

# —————————————–

class IntegratedWeatherSystem(QWidget):

    def __init__(self):

        super().__init__()

        self.settings = QSettings(‘WeatherApp’, ‘IntegratedSystem’)

        self.npz_dir = self.settings.value(‘npz_dir’, os.path.join(os.getcwd(), “gpv_cache_npz”))

        os.makedirs(self.npz_dir, exist_ok=True)

        create_default_points_csv()

        self.init_sqlite_table()

        self.init_ui()

        self.load_points_from_csv()

    def init_sqlite_table(self):

        conn = sqlite3.connect(DB_FILE)

        c = conn.cursor()

        c.execute(”’

            CREATE TABLE IF NOT EXISTS weather_data (

                id INTEGER PRIMARY KEY AUTOINCREMENT,

                model TEXT,

                init_time TEXT,

                valid_time TEXT,

                forecast_time INTEGER,

                lat REAL,

                lon REAL,

                precip REAL,

                temp REAL,

                UNIQUE(model, init_time, valid_time, lat, lon)

            )

        ”’)

        conn.commit()

        conn.close()

    def init_ui(self):

        self.setWindowTitle(“気象局地分析データベース & 予報出力システム (Ver 1.00)”)

        self.resize(1100, 750)

        self.setStyleSheet(“””

            QWidget { background-color: #1E1E24; color: #E0E0E6; font-family: ‘MS Gothic’; font-size: 10pt; }

            QTabWidget::pane { border: 1px solid #3A3A42; background: #25252D; }

            QTabBar::tab { background: #2D2D35; color: #AAAAAA; padding: 10px 20px; border: 1px solid #3A3A42; }

            QTabBar::tab:selected { background: #25252D; color: #64FFDA; font-weight: bold; border-top: 2px solid #64FFDA; }

            QPushButton { background-color: #0E639C; border: none; padding: 8px 15px; border-radius: 4px; color: white; font-weight: bold; }

            QPushButton:hover { background-color: #1177BB; }

            QComboBox, QLineEdit, QDateTimeEdit { background-color: #2D2D35; border: 1px solid #4A4A54; padding: 4px; color: white; }

            QTableWidget { background-color: #1E1E24; gridline-color: #3A3A42; color: #E0E0E6; }

            QHeaderView::section { background-color: #2D2D35; color: #64FFDA; font-weight: bold; border: 1px solid #3A3A42; }

            QListWidget { background-color: #1E1E24; border: 1px solid #3A3A42; }

        “””)

        main_layout = QVBoxLayout(self)

        # トップ共有バー(監視フォルダとロボットの起動状態)

        top_bar = QHBoxLayout()

        self.lbl_folder = QLabel(f”現在の監視フォルダ: {self.npz_dir}”)

        btn_folder = QPushButton(“📁 フォルダ変更”)

        btn_folder.clicked.connect(self.change_monitor_folder)

        self.btn_robot = QPushButton(“▶ 自動登録ロボット: OFF”)

        self.btn_robot.setCheckable(True)

        self.btn_robot.clicked.connect(self.toggle_robot)

        top_bar.addWidget(self.lbl_folder, stretch=5)

        top_bar.addWidget(btn_folder, stretch=1)

        top_bar.addWidget(self.btn_robot, stretch=2)

        main_layout.addLayout(top_bar)

        self.progress_bar = QProgressBar()

        self.progress_bar.setFixedHeight(12)

        self.progress_bar.setValue(0)

        main_layout.addWidget(self.progress_bar)

        # タブコントロール

        self.tabs = QTabWidget()

        self.setup_tab_db()       # タブ1: DB管理・統計

        self.setup_tab_graph()    # タブ2: グラフ表示分析

        self.setup_tab_export()   # タブ3: 予報点Excel出力

        main_layout.addWidget(self.tabs)

    # —————————————–

    # タブ1: DB管理・統計

    # —————————————–

    def setup_tab_db(self):

        tab = QWidget()

        layout = QVBoxLayout(tab)

        # 検索フィルターエリア

        filter_layout = QHBoxLayout()

        self.cb_db_model = QComboBox()

        self.cb_db_model.addItems([“すべて”, “MSM”, “GSM_JP”, “MSM_GUID”, “GSM_GUID”])

        filter_layout.addWidget(QLabel(“モデル:”))

        filter_layout.addWidget(self.cb_db_model)

        btn_search = QPushButton(“🔍 条件抽出実行”)

        btn_search.clicked.connect(self.query_database_table)

        filter_layout.addWidget(btn_search)

        btn_stats = QPushButton(“📊 選択範囲の簡易統計”)

        btn_stats.clicked.connect(self.calculate_table_statistics)

        btn_stats.setStyleSheet(“background-color: #8E44AD;”)

        filter_layout.addWidget(btn_stats)

        filter_layout.addStretch()

        layout.addLayout(filter_layout)

        # テーブル表示

        self.table_db = QTableWidget()

        self.table_db.setColumnCount(7)

        self.table_db.setHorizontalHeaderLabels([“モデル”, “初期時間(JST)”, “予報時間(JST)”, “FT”, “緯度”, “経度”, “降水量(mm)”])

        self.table_db.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)

        self.table_db.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)

        layout.addWidget(self.table_db)

        # リアルタイムログ

        self.log_list = QListWidget()

        self.log_list.setFixedHeight(100)

        layout.addWidget(QLabel(“■ システムログ”))

        layout.addWidget(self.log_list)

        self.tabs.addTab(tab, “🗄️ DB管理・統計”)

    # —————————————–

    # タブ2: 可視化グラフ表示

    # —————————————–

    def setup_tab_graph(self):

        tab = QWidget()

        layout = QHBoxLayout(tab)

        # 左側コントロール

        ctrl_panel = QVBoxLayout()

        ctrl_panel.addWidget(QLabel(“■ 地点選択”))

        self.cb_graph_point = QComboBox()

        ctrl_panel.addWidget(self.cb_graph_point)

        ctrl_panel.addWidget(QLabel(“■ モデル選択”))

        self.cb_graph_model = QComboBox()

        self.cb_graph_model.addItems([“MSM”, “GSM_JP”, “MSM_GUID”, “GSM_GUID”])

        ctrl_panel.addWidget(self.cb_graph_model)

        ctrl_panel.addWidget(QLabel(“■ 初期時刻(JST)”))

        self.de_graph_init = QDateTimeEdit(QDateTime.currentDateTime())

        self.de_graph_init.setDisplayFormat(“yyyy/MM/dd HH:00”)

        ctrl_panel.addWidget(self.de_graph_init)

        btn_draw = QPushButton(“📈 時系列グラフ描画”)

        btn_draw.setStyleSheet(“background-color: #27AE60;”)

        btn_draw.clicked.connect(self.draw_meteogram_chart)

        ctrl_panel.addWidget(btn_draw)

        ctrl_panel.addStretch()

        layout.addLayout(ctrl_panel, stretch=2)

        # 右側キャンバス

        self.graph_fig = Figure(figsize=(6, 4), facecolor=’#25252D’)

        self.graph_canvas = FigureCanvas(self.graph_fig)

        layout.addWidget(self.graph_canvas, stretch=8)

        self.tabs.addTab(tab, “📊 分析グラフ表示”)

    # —————————————–

    # タブ3: 予報点Excel一括出力

    # —————————————–

    def setup_tab_export(self):

        tab = QWidget()

        layout = QVBoxLayout(tab)

        info = QLabel(“【市内予報点 ピンポイントExcel出力システム】\n「forecast_points.csv」に定義された全地点のデータを抽出し、実務用のフォーマットへ変換して一括出力します。”)

        info.setStyleSheet(“color: #64FFDA; font-weight: bold; font-size: 11pt;”)

        layout.addWidget(info)

        config_panel = QHBoxLayout()

        self.cb_exp_model = QComboBox()

        self.cb_exp_model.addItems([“MSM”, “GSM_JP”, “MSM_GUID”, “GSM_GUID”])

        self.cb_exp_step = QComboBox()

        self.cb_exp_step.addItems([“1時間間隔”, “3時間間隔”, “6時間間隔”])

        config_panel.addWidget(QLabel(“出力モデル:”))

        config_panel.addWidget(self.cb_exp_model)

        config_panel.addWidget(QLabel(“時間間隔:”))

        config_panel.addWidget(self.cb_exp_step)

        self.de_exp_init = QDateTimeEdit(QDateTime.currentDateTime())

        self.de_exp_init.setDisplayFormat(“yyyy/MM/dd HH:00”)

        config_panel.addWidget(QLabel(“初期時刻(JST):”))

        config_panel.addWidget(self.de_exp_init)

        config_panel.addStretch()

        layout.addLayout(config_panel)

        # CSVからロードした地点のプレビュー

        layout.addWidget(QLabel(“■ 出力対象地点の一覧 (forecast_points.csv から自動ロード)”))

        self.table_points_preview = QTableWidget()

        self.table_points_preview.setColumnCount(3)

        self.table_points_preview.setHorizontalHeaderLabels([“地点名”, “緯度”, “経度”])

        self.table_points_preview.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)

        layout.addWidget(self.table_points_preview)

        btn_excel = QPushButton(“📥 指定条件でExcel予報帳票を出力 (一括保存)”)

        btn_excel.setStyleSheet(“background-color: #D35400; font-size: 11pt; padding: 12px;”)

        btn_excel.clicked.connect(self.export_forecast_to_excel)

        layout.addWidget(btn_excel)

        self.tabs.addTab(tab, “📋 予報帳票・Excel出力”)

    # —————————————–

    # ロジック・処理関数

    # —————————————–

    def log(self, msg):

        self.log_list.addItem(f”[{datetime.now().strftime(‘%H:%M:%S’)}] {msg}”)

        self.log_list.scrollToBottom()

    def change_monitor_folder(self):

        d = QFileDialog.getExistingDirectory(self, “監視フォルダの変更”, self.npz_dir)

        if d:

            self.npz_dir = d

            self.settings.setValue(‘npz_dir’, self.npz_dir)

            self.lbl_folder.setText(f”現在の監視フォルダ: {self.npz_dir}”)

            self.log(f”監視フォルダを変更しました: {d}”)

    def toggle_robot(self):

        if self.btn_robot.isChecked():

            self.btn_robot.setText(“🛑 自動登録ロボット: 稼働中”)

            self.btn_robot.setStyleSheet(“background-color: #E63946; color: white;”)

            self.robot_thread = DBAutoLoaderWorker(self.npz_dir)

            self.robot_thread.log_signal.connect(self.log)

            self.robot_thread.progress_signal.connect(lambda c, t: self.progress_bar.setValue(int(c/t*100) if t>0 else 0))

            self.robot_thread.start()

        else:

            self.btn_robot.setText(“▶ 自動登録ロボット: OFF”)

            self.btn_robot.setStyleSheet(“”)

            self.robot_thread.stop()

            self.robot_thread.wait()

            self.progress_bar.setValue(0)

    def load_points_from_csv(self):

        self.cb_graph_point.clear()

        self.table_points_preview.setRowCount(0)

        if not os.path.exists(CSV_FILE): return

        try:

            with open(CSV_FILE, mode=’r’, encoding=’utf-8′) as f:

                reader = csv.reader(f)

                header = next(reader) # ヘッダーをスキップ

                for idx, row in enumerate(reader):

                    if len(row) < 3: continue

                    name, lat, lon = row[0], row[1], row[2]

                    self.cb_graph_point.addItem(name, (float(lat), float(lon)))

                    self.table_points_preview.insertRow(idx)

                    self.table_points_preview.setItem(idx, 0, QTableWidgetItem(name))

                    self.table_points_preview.setItem(idx, 1, QTableWidgetItem(lat))

                    self.table_points_preview.setItem(idx, 2, QTableWidgetItem(lon))

            self.log(f”📂 地点マスタCSVから {self.cb_graph_point.count()} 箇所の予報点をロードしました。”)

        except Exception as e:

            self.log(f”⚠️ CSVロードエラー: {e}”)

    # 【タブ1ロジック】抽出機能

    def query_database_table(self):

        model_filter = self.cb_db_model.currentText()

        conn = sqlite3.connect(DB_FILE)

        c = conn.cursor()

        if model_filter == “すべて”:

            c.execute(“SELECT model, init_time, valid_time, forecast_time, lat, lon, precip FROM weather_data ORDER BY valid_time DESC LIMIT 500”)

        else:

            c.execute(“SELECT model, init_time, valid_time, forecast_time, lat, lon, precip FROM weather_data WHERE model=? ORDER BY valid_time DESC LIMIT 500”, (model_filter,))

        rows = c.fetchall()

        conn.close()

        self.table_db.setRowCount(0)

        for idx, r in enumerate(rows):

            self.table_db.insertRow(idx)

            # 内部時間(UTC)から表示用(JST)へ変換変換

            init_jst = (datetime.fromisoformat(r[1]) + timedelta(hours=9)).strftime(‘%m/%d %H:%M’)

            valid_jst = (datetime.fromisoformat(r[2]) + timedelta(hours=9)).strftime(‘%m/%d %H:%M’)

            self.table_db.setItem(idx, 0, QTableWidgetItem(r[0]))

            self.table_db.setItem(idx, 1, QTableWidgetItem(init_jst))

            self.table_db.setItem(idx, 2, QTableWidgetItem(valid_jst))

            self.table_db.setItem(idx, 3, QTableWidgetItem(str(r[3])))

            self.table_db.setItem(idx, 4, QTableWidgetItem(f”{r[4]:.2f}”))

            self.table_db.setItem(idx, 5, QTableWidgetItem(f”{r[5]:.2f}”))

            self.table_db.setItem(idx, 6, QTableWidgetItem(f”{r[6]:.1f}”))

        self.log(f”🔍 DBから {len(rows)} 件のデータを抽出表示しました。”)

    # 【タブ1ロジック】統計機能

    def calculate_table_statistics(self):

        selected_ranges = self.table_db.selectedRanges()

        if not selected_ranges:

            QMessageBox.information(self, “統計”, “集計したい降水量のセル範囲を選択してください。”)

            return

        precips = []

        for r in selected_ranges:

            for row in range(r.topRow(), r.bottomRow() + 1):

                item = self.table_db.item(row, 6) # 降水量のカラム

                if item:

                    try: precips.append(float(item.text()))

                    except: pass

        if not precips: return

        arr = np.array(precips)

        msg = f”【選択範囲の統計結果 (サンプル数: {len(arr)})】\n\n”

        msg += f”■ 積算降水量: {np.sum(arr):.1f} mm\n”

        msg += f”■ 最大降水量: {np.max(arr):.1f} mm\n”

        msg += f”■ 平均降水量: {np.mean(arr):.1f} mm”

        QMessageBox.information(self, “統計分析結果”, msg)

    # 【タブ2ロジック】時系列グラフ表示

    def draw_meteogram_chart(self):

        self.graph_fig.clear()

        ax = self.graph_fig.add_subplot(111, facecolor=’#1E1E24′)

        ax.tick_params(colors=’white’)

        ax.xaxis.label.set_color(‘white’)

        ax.yaxis.label.set_color(‘white’)

        # 本来はSQLiteから選択地点・選択モデル・選択初期時間の時系列を引っ張ります

        # ここではプロトタイプ用の即時描画モックアップ

        now_dt = self.de_graph_init.dateTime().toPyDateTime()

        dates = [now_dt + timedelta(hours=i) for i in range(24)]

        mock_precip = np.random.exponential(scale=1.0, size=24)

        mock_precip[mock_precip < 0.5] = 0.0 # 空振りを作る

        ax.bar(dates, mock_precip, width=0.03, color=’#38bdf8′, alpha=0.8, label=’予測降水量’)

        ax.set_ylabel(“降水量 (mm)”)

        ax.set_title(f”{self.cb_graph_point.currentText()} 時系列分析グラフ”, color=’white’, weight=’bold’, fontsize=12)

        ax.xaxis.set_major_formatter(mdates.DateFormatter(‘%m/%d\n%H:00’))

        self.graph_fig.autofmt_xdate()

        ax.grid(True, color=’#3A3A42′, linestyle=’–‘)

        self.graph_canvas.draw()

        self.log(f”📈 {self.cb_graph_point.currentText()} のグラフを描画しました。”)

    # 【タブ3ロジック】Excel出力帳票出力機能

    def export_forecast_to_excel(self):

        model = self.cb_exp_model.currentText()

        init_jst = self.de_exp_init.dateTime().toPyDateTime()

        # データベース内部検索用:JSTからUTCへ逆変換してベース時間を揃える

        init_utc = init_jst – timedelta(hours=9)

        desktop = os.path.expanduser(“~/Desktop”)

        out_filename = f”予報点一括出力_{model}_{init_jst.strftime(‘%m%d_%H’)}JST.xlsx”

        save_path, _ = QFileDialog.getSaveFileName(self, “Excel帳票を保存”, os.path.join(desktop, out_filename), “Excel Files (*.xlsx)”)

        if not save_path: return

        try:

            import openpyxl

            from openpyxl.styles import Font, PatternFill, Alignment, Border, Side

            wb = openpyxl.Workbook()

            ws = wb.active

            ws.title = f”{model}_予報シート”

            # スタイル設定

            font_title = Font(name=’MS Gothic’, size=14, bold=True, color=’1D3557′)

            font_header = Font(name=’MS Gothic’, size=10, bold=True, color=’FFFFFF’)

            font_data = Font(name=’MS Gothic’, size=10)

            fill_header = PatternFill(start_color=’1D3557′, end_color=’1D3557′, fill_type=’solid’)

            align_center = Alignment(horizontal=’center’, vertical=’center’)

            border_thin = Border(left=Side(style=’thin’, color=’DDDDDD’), right=Side(style=’thin’, color=’DDDDDD’),

                                 top=Side(style=’thin’, color=’DDDDDD’), bottom=Side(style=’thin’, color=’DDDDDD’))

            ws.append([f”■ 札幌市内・近郊 30予報点一括予報帳票 ({model})”])

            ws.cell(row=1, column=1).font = font_title

            ws.append([f”初期時刻(JST): {init_jst.strftime(‘%Y/%m/%d %H:00’)} JST  (データ検索基準: {init_utc.strftime(‘%Y-%m-%d %H:%M:%S’)} UTC)”])

            ws.append([]) # 空行

            # ヘッダー

            headers = [“地点名”, “緯度”, “経度”, “FT=03h”, “FT=06h”, “FT=09h”, “FT=12h”, “FT=15h”, “FT=18h”, “FT=21h”, “FT=24h”]

            ws.append(headers)

            for col_idx in range(1, len(headers) + 1):

                cell = ws.cell(row=4, column=col_idx)

                cell.font = font_header

                cell.fill = fill_header

                cell.alignment = align_center

            # 各地点ごとのデータ書き込み

            conn = sqlite3.connect(DB_FILE)

            c = conn.cursor()

            # CSVの内容に沿って全地点を走査

            for p_idx in range(self.table_points_preview.rowCount()):

                name = self.table_points_preview.item(p_idx, 0).text()

                lat = float(self.table_points_preview.item(p_idx, 1).text())

                lon = float(self.table_points_preview.item(p_idx, 2).text())

                row_data = [name, lat, lon]

                # FTごとにデータベースから値を抽出(UTCベースの検索)

                for ft in [3, 6, 9, 12, 15, 18, 21, 24]:

                    target_valid_utc = init_utc + timedelta(hours=ft)

                    # 実際は1km四方の直近緯度経度をSQLの範囲検索で探します

                    c.execute(“””SELECT precip FROM weather_data

                                 WHERE model=? AND init_time=? AND forecast_time=?

                                 LIMIT 1″””, (model, init_utc.isoformat(), ft))

                    res = c.fetchone()

                    if res: row_data.append(f”{res[0]:.1f}”)

                    else: row_data.append(“-“) # データが未ロードの場合はハイフン

                ws.append(row_data)

            conn.close()

            # データセルの装飾

            for row in ws.iter_rows(min_row=5, max_row=ws.max_row, min_col=1, max_col=len(headers)):

                for cell in row:

                    cell.font = font_data

                    cell.border = border_thin

                    cell.alignment = align_center

            wb.save(save_path)

            self.log(f”✅ Excel予報帳票を完全出力しました: {os.path.basename(save_path)}”)

            QMessageBox.information(self, “出力完了”, f”デスクトップ等へ正常に一括出力されました:\n{os.path.basename(save_path)}”)

        except Exception as e:

            self.log(f”⚠️ Excel出力エラー: {e}”)

            traceback.print_exc()

if __name__ == ‘__main__’:

    app = QApplication(sys.argv)

    window = IntegratedWeatherSystem()

    window.show()

    sys.exit(app.exec())

コメント