未分類

import sys
import os
import glob
import re
import sqlite3
import numpy as np
import pandas as pd
from datetime import datetime, timedelta

from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
                             QPushButton, QLabel, QTableWidget, QTableWidgetItem, QTabWidget, 
                             QHeaderView, QFileDialog, QMessageBox, QGroupBox, QComboBox, 
                             QProgressBar, QLineEdit, QMenu, QDialog)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QAction

DEFAULT_DB_PATH = os.path.join(os.getcwd(), "sapporo_ml_ready_archive.db")

# ==========================================
# カスタムダイアログ: エクセル風「▽」フィルター
# ==========================================
class FilterDialog(QDialog):
    def __init__(self, col_name, parent=None):
        super().__init__(parent)
        self.setWindowTitle(f"🔍 '{col_name}' フィルター設定")
        self.setFixedSize(300, 150)
        layout = QVBoxLayout(self)
        
        layout.addWidget(QLabel(f"条件を指定してください:"))
        h_layout = QHBoxLayout()
        self.op_combo = QComboBox()
        self.op_combo.addItems(["=", ">", ">=", "<", "<=", "LIKE", "!="])
        h_layout.addWidget(self.op_combo)
        
        self.val_input = QLineEdit()
        self.val_input.setPlaceholderText("値 (例: GSM_GPV または 10.5)")
        h_layout.addWidget(self.val_input)
        layout.addLayout(h_layout)
        
        btn = QPushButton("✅ 適用")
        btn.setStyleSheet("background-color: #27ae60; font-weight: bold; padding: 5px;")
        btn.clicked.connect(self.accept)
        layout.addWidget(btn)

    def get_condition(self):
        op = self.op_combo.currentText()
        val = self.val_input.text().strip()
        if not val: return None
        try:
            float(val)
            return f"{op} {val}"
        except:
            return f"{op} '{val}'"

# ==========================================
# バックグラウンド処理: 領域指定型ワイドDB生成
# ==========================================
class DBGeneratorThread(QThread):
    progress = pyqtSignal(int, str)
    finished = pyqtSignal(str)
    error = pyqtSignal(str)

    def __init__(self, npz_dir, db_path):
        super().__init__()
        self.npz_dir = npz_dir
        self.db_path = db_path

    def run(self):
        conn = None
        
        # --- 完璧な湿数と相当温位を再計算する安全な関数 ---
        def calc_tddep_and_ept(t_arr, rh_arr, lvl, num_pts, mask):
            if t_arr is None or rh_arr is None: return [None]*num_pts, [None]*num_pts
            t = t_arr.flatten()[mask]
            rh = rh_arr.flatten()[mask]
            
            # ケルビンの場合は摂氏に補正
            t_c = np.where(t > 150, t - 273.15, t)
            # RHが1.5以下(割合)の場合は100倍して%に補正
            rh_percent = np.where(rh <= 1.5, rh * 100.0, rh)
            rh_c = np.clip(rh_percent, 0.1, 100.0)
            
            # テテンスの式による水蒸気圧と露点温度の算出
            e = 6.112 * np.exp((17.67 * t_c) / (t_c + 243.5)) * (rh_c / 100.0)
            td = (243.5 * np.log(e / 6.112)) / (17.67 - np.log(e / 6.112))
            tddep = t_c - td # 湿数
            
            # 相当温位の算出
            tk = t_c + 273.15
            theta = tk * ((1000.0 / lvl) ** 0.2854)
            w_mix = 0.622 * e / (lvl - e)
            ept = theta * np.exp((2.5e6 * w_mix) / (1004.0 * tk))
            
            def format_arr(arr, min_v, max_v):
                res = arr.astype(object)
                invalid = np.isnan(arr) | (arr > max_v) | (arr < min_v)
                res[invalid] = None
                res[~invalid] = np.round(arr[~invalid].astype(float), 3)
                return res.tolist()
                
            return format_arr(tddep, -50, 100), format_arr(ept, 200, 500)

        # --- 気温減率(下層 - 上層)を計算する関数 ---
        def calc_lapse(t_lower, t_upper, num_pts, mask):
            if t_lower is None or t_upper is None: return [None]*num_pts
            # 下層の温度 - 上層の温度 (正の値が大きいほど不安定)
            diff = t_lower.flatten()[mask] - t_upper.flatten()[mask]
            res = diff.astype(object)
            invalid = np.isnan(diff)
            res[invalid] = None
            res[~invalid] = np.round(diff[~invalid].astype(float), 3)
            return res.tolist()

        try:
            self.progress.emit(5, "ML Readyデータベースの初期化中...")
            conn = sqlite3.connect(self.db_path, timeout=15)
            cursor = conn.cursor()
            
            # ssi を lapse (気温減率) に完全置換したスキーマ
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS weather_master (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    model_type TEXT, init_time TEXT, valid_time TEXT, ft INTEGER,
                    lat REAL, lon REAL,
                    
                    surf_temp REAL, surf_u REAL, surf_v REAL, surf_rain REAL, surf_snow REAL,
                    cloud_tot REAL, cloud_low REAL, cloud_mid REAL, cloud_high REAL,
                    
                    gh_975 REAL, gh_950 REAL, gh_925 REAL, gh_850 REAL, gh_700 REAL, gh_500 REAL,
                    u_975 REAL, u_950 REAL, u_925 REAL, u_850 REAL, u_700 REAL, u_500 REAL,
                    v_975 REAL, v_950 REAL, v_925 REAL, v_850 REAL, v_700 REAL, v_500 REAL,
                    
                    tddep_950 REAL, tddep_925 REAL, tddep_850 REAL, tddep_700 REAL,
                    ept_950 REAL, ept_925 REAL, ept_850 REAL,
                    w_700 REAL, vort_500 REAL,
                    
                    lapse_850_500 REAL, lapse_850_700 REAL, lapse_925_700 REAL,
                    
                    UNIQUE(model_type, lat, lon, valid_time, ft)
                )
            ''')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_time_loc ON weather_master (valid_time, lat, lon)')
            
            npz_files = glob.glob(os.path.join(self.npz_dir, "*.npz"))
            if not npz_files:
                self.error.emit(f"エラー: {self.npz_dir} に .npz ファイルが存在しません。")
                return

            target_files = [
                f for f in npz_files 
                if ("GSM_JP_" in os.path.basename(f) or "MSM_" in os.path.basename(f)) 
                and "GUID" not in os.path.basename(f)
            ]
            
            total_files = len(target_files)
            if total_files == 0:
                self.error.emit("抽出対象のGPVデータ(GSM/MSM)が見つかりません。")
                return

            total_records_inserted = 0
            self.progress.emit(10, f"領域限定(N40-46, E138-146)の特徴量抽出を開始... 対象: {total_files}ファイル")

            for i, fpath in enumerate(target_files):
                fname = os.path.basename(fpath)
                m_model = "MSM" if "MSM" in fname else "GSM"
                model_type = f"{m_model}_GPV"
                
                time_match = re.search(r'_(\d{14})_', fname)
                ft_match = re.search(r'_FT(\d+)', fname)
                if not time_match or not ft_match: continue
                
                init_time_str = time_match.group(1)
                ft_val = int(ft_match.group(1))
                
                dt_init = datetime.strptime(init_time_str, "%Y%m%d%H%M%S")
                dt_valid = dt_init + timedelta(hours=ft_val)
                str_init = dt_init.strftime("%Y-%m-%d %H:%M:%S")
                str_valid = dt_valid.strftime("%Y-%m-%d %H:%M:%S")
                
                try:
                    data = np.load(fpath)
                    files = data.files
                    
                    lon_key = next((k for k in files if 'lon' in k.lower()), None)
                    lat_key = next((k for k in files if 'lat' in k.lower()), None)
                    if not lon_key or not lat_key: continue
                    lon_arr, lat_arr = data[lon_key], data[lat_key]
                    if lon_arr.ndim == 1: lon_2d, lat_2d = np.meshgrid(lon_arr, lat_arr)
                    else: lon_2d, lat_2d = lon_arr, lat_arr
                    
                    lats_flat = np.round(lat_2d.flatten(), 4)
                    lons_flat = np.round(lon_2d.flatten(), 4)
                    
                    mask = (lats_flat >= 40.0) & (lats_flat <= 46.0) & (lons_flat >= 138.0) & (lons_flat <= 146.0)
                    
                    lats = lats_flat[mask]
                    lons = lons_flat[mask]
                    num_pts = len(lats)
                    
                    if num_pts == 0:
                        data.close()
                        continue

                    def get_v_flat(key_opts):
                        k = next((x for x in key_opts if x in files), None)
                        if not k: return [None] * num_pts
                        arr_flat = data[k].flatten()
                        masked_arr = arr_flat[mask]
                        res = masked_arr.astype(object)
                        invalid = np.isnan(masked_arr) | (masked_arr > 200000) | (masked_arr < -10000)
                        res[invalid] = None
                        res[~invalid] = np.round(masked_arr[~invalid].astype(float), 3)
                        return res.tolist()
                        
                    def get_raw_arr(key_opts):
                        k = next((x for x in key_opts if x in files), None)
                        if k: return data[k]
                        return None

                    s_t = get_v_flat(['t2m', 'tmp', 'temp'])
                    s_u = get_v_flat(['u10', 'u', 'ugrd'])
                    s_v = get_v_flat(['v10', 'v', 'vgrd'])
                    s_r = get_v_flat(['precip', 'tp', 'apcp'])
                    s_s = get_v_flat(['snow', 'weasd', 'snod', 'asnow'])
                    
                    c_t = get_v_flat(['tcc', 'tcdc', 'TCC', 'TCDC', 'var_0_6_1', 'var_0_6_192'])
                    c_l = get_v_flat(['lcc', 'LCC', 'var_0_6_3'])
                    c_m = get_v_flat(['mcc', 'MCC', 'var_0_6_4'])
                    c_h = get_v_flat(['hcc', 'HCC', 'var_0_6_5'])
                    
                                        
                    gh_975 = get_v_flat(['gh975', 'z975']); u_975 = get_v_flat(['u975']); v_975 = get_v_flat(['v975'])
                    gh_950 = get_v_flat(['gh950', 'z950']); u_950 = get_v_flat(['u950']); v_950 = get_v_flat(['v950'])
                    gh_925 = get_v_flat(['gh925', 'z925']); u_925 = get_v_flat(['u925']); v_925 = get_v_flat(['v925'])
                    gh_850 = get_v_flat(['gh850', 'z850']); u_850 = get_v_flat(['u850']); v_850 = get_v_flat(['v850'])
                    gh_700 = get_v_flat(['gh700', 'z700']); u_700 = get_v_flat(['u700']); v_700 = get_v_flat(['v700'])
                    gh_500 = get_v_flat(['gh500', 'z500']); u_500 = get_v_flat(['u500']); v_500 = get_v_flat(['v500'])
                    
                    w_700 = get_v_flat(['w700']); vort_500 = get_v_flat(['vort500'])
                    
                    # 湿数・相当温位の動的再計算
                    t950_arr = get_raw_arr(['t950', 'tmp950']); r950_arr = get_raw_arr(['r950', 'rh950'])
                    td_950, ep_950 = calc_tddep_and_ept(t950_arr, r950_arr, 950, num_pts, mask)
                    
                    t925_arr = get_raw_arr(['t925', 'tmp925']); r925_arr = get_raw_arr(['r925', 'rh925'])
                    td_925, ep_925 = calc_tddep_and_ept(t925_arr, r925_arr, 925, num_pts, mask)
                    
                    t850_arr = get_raw_arr(['t850', 'tmp850']); r850_arr = get_raw_arr(['r850', 'rh850'])
                    td_850, ep_850 = calc_tddep_and_ept(t850_arr, r850_arr, 850, num_pts, mask)
                    
                    t700_arr = get_raw_arr(['t700', 'tmp700']); r700_arr = get_raw_arr(['r700', 'rh700'])
                    td_700, _ = calc_tddep_and_ept(t700_arr, r700_arr, 700, num_pts, mask)
                    
                    # 気温減率(大気不安定度)の算出
                    t500_arr = get_raw_arr(['t500', 'tmp500'])
                    lapse_850_500 = calc_lapse(t850_arr, t500_arr, num_pts, mask)
                    lapse_850_700 = calc_lapse(t850_arr, t700_arr, num_pts, mask)
                    lapse_925_700 = calc_lapse(t925_arr, t700_arr, num_pts, mask)

                    model_list = [model_type] * num_pts
                    init_list = [str_init] * num_pts
                    valid_list = [str_valid] * num_pts
                    ft_list = [ft_val] * num_pts

                    rows = zip(
                        model_list, init_list, valid_list, ft_list, lats, lons,
                        s_t, s_u, s_v, s_r, s_s, c_t, c_l, c_m, c_h,
                        gh_975, gh_950, gh_925, gh_850, gh_700, gh_500,
                        u_975, u_950, u_925, u_850, u_700, u_500,
                        v_975, v_950, v_925, v_850, v_700, v_500,
                        td_950, td_925, td_850, td_700,
                        ep_950, ep_925, ep_850,
                        w_700, vort_500,
                        lapse_850_500, lapse_850_700, lapse_925_700
                    )

                    cursor.executemany('''
                        INSERT OR REPLACE INTO weather_master (
                            model_type, init_time, valid_time, ft, lat, lon,
                            surf_temp, surf_u, surf_v, surf_rain, surf_snow,
                            cloud_tot, cloud_low, cloud_mid, cloud_high,
                            gh_975, gh_950, gh_925, gh_850, gh_700, gh_500,
                            u_975, u_950, u_925, u_850, u_700, u_500,
                            v_975, v_950, v_925, v_850, v_700, v_500,
                            tddep_950, tddep_925, tddep_850, tddep_700,
                            ept_950, ept_925, ept_850,
                            w_700, vort_500,
                            lapse_850_500, lapse_850_700, lapse_925_700
                        ) VALUES (?,?,?,?,?,?, ?,?,?,?,?, ?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?, ?,?,?, ?,?, ?,?,?)
                    ''', rows)
                    conn.commit()
                    total_records_inserted += num_pts
                    data.close()

                except Exception as e:
                    print(f"Error {fname}: {e}")
                
                if i % max(1, total_files // 20) == 0:
                    self.progress.emit(10 + int((i / total_files) * 85), f"抽出中... {i+1}/{total_files} ファイル")

            self.progress.emit(100, "処理完了")
            self.finished.emit(f"✓ DB更新完了: 指定領域内の {total_records_inserted:,} レコードを処理しました。")
            
        except Exception as e:
            self.error.emit(f"DB生成エラー: {str(e)}")
        finally:
            if conn:
                conn.close() 

# ==========================================
# メインアプリケーション UIとロジック
# ==========================================
class IntegratedWeatherSystem(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("MLデータパイプライン & SQLite エクセル風ブラウザ (Ver 10.3 湿数・大気安定度自動再計算版)")
        self.resize(1400, 900)
        self.setStyleSheet("background-color: #2b2b2b; color: #e0e0e0; font-family: 'MS Gothic';")
        
        self.current_db_path = DEFAULT_DB_PATH
        self.current_npz_dir = os.path.join(os.getcwd(), "gpv_cache_npz")
        
        self.current_page = 1
        self.records_per_page = 500
        self.total_records = 0
        self.is_loading_table = False
        self.active_filters = {} 
        
        self.init_ui()
        if os.path.exists(self.current_db_path):
            self.update_browser_tables()

    def init_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(central_widget)
        
        db_group = QGroupBox("🗄 接続データベース")
        db_group.setStyleSheet("QGroupBox { border: 1px solid #555; font-weight: bold; }")
        db_layout = QHBoxLayout(db_group)
        self.btn_sel_db = QPushButton("📁 DBファイルを選択 / 新規作成")
        self.btn_sel_db.setStyleSheet("background-color: #34495e; padding: 6px;")
        self.btn_sel_db.clicked.connect(self.select_db_file)
        self.lbl_db_path = QLabel(f"現在のDB: {self.current_db_path}")
        self.lbl_db_path.setStyleSheet("color: #00fa9a; font-weight: bold;")
        db_layout.addWidget(self.btn_sel_db)
        db_layout.addWidget(self.lbl_db_path, 1)
        main_layout.addWidget(db_group)
        
        self.tabs = QTabWidget()
        self.tabs.setStyleSheet("""
            QTabBar::tab { background-color: #3c3f41; padding: 10px; font-weight: bold; font-size: 10pt; }
            QTabBar::tab:selected { background-color: #007acc; color: white; }
            QTabWidget::pane { border: 1px solid #555; }
        """)
        
        self.tab1_npz = QWidget()
        self.tab2_obs = QWidget()
        self.tab3_browser = QWidget()
        
        self.tabs.addTab(self.tab1_npz, "📥 [Tab 1] GPV(GSM/MSM) 領域限定DB化")
        self.tabs.addTab(self.tab2_obs, "📝 [Tab 2] 観測点CSVデータインポート")
        self.tabs.addTab(self.tab3_browser, "🔍 [Tab 3] エクセル風 SQLite ブラウザ")
        
        main_layout.addWidget(self.tabs)
        
        self.setup_tab1_npz()
        self.setup_tab2_obs()
        self.setup_tab3_browser()

    def setup_tab1_npz(self):
        layout = QVBoxLayout(self.tab1_npz)
        npz_layout = QHBoxLayout()
        self.btn_sel_npz = QPushButton("📁 NPZフォルダを選択")
        self.btn_sel_npz.setStyleSheet("background-color: #2980b9; padding: 6px;")
        self.btn_sel_npz.clicked.connect(self.select_npz_dir)
        self.lbl_npz_dir = QLabel(f"NPZ: {self.current_npz_dir}")
        npz_layout.addWidget(self.btn_sel_npz)
        npz_layout.addWidget(self.lbl_npz_dir, 1)
        layout.addLayout(npz_layout)
        
        self.btn_generate_db = QPushButton("⚡ 本番: 【GSM/MSM】を【N40-46, E138-146】の範囲でDB抽出")
        self.btn_generate_db.setStyleSheet("background-color: #c0392b; font-size: 12pt; font-weight: bold; padding: 15px;")
        self.btn_generate_db.clicked.connect(self.start_db_generation)
        layout.addWidget(self.btn_generate_db)
        
        self.progress_bar = QProgressBar()
        self.progress_bar.setVisible(False)
        layout.addWidget(self.progress_bar)
        
        self.lbl_status = QLabel("ステータス: 待機中")
        layout.addWidget(self.lbl_status)
        layout.addStretch()

    def setup_tab2_obs(self):
        layout = QVBoxLayout(self.tab2_obs)
        ctrl_layout = QHBoxLayout()
        self.btn_load_csv = QPushButton("📁 観測データ(CSV)を読込")
        self.btn_load_csv.setStyleSheet("background-color: #27ae60; font-weight: bold; padding: 8px;")
        self.btn_load_csv.clicked.connect(self.preview_csv)
        ctrl_layout.addWidget(self.btn_load_csv)
        
        ctrl_layout.addWidget(QLabel("挿入先テーブル名:"))
        self.txt_table_name = QLineEdit("obs_master")
        self.txt_table_name.setStyleSheet("background-color: #1e1e1e; padding: 5px;")
        ctrl_layout.addWidget(self.txt_table_name)
        
        self.btn_import_csv = QPushButton("💾 表示中のデータをDBにインポート")
        self.btn_import_csv.setStyleSheet("background-color: #8e44ad; font-weight: bold; padding: 8px;")
        self.btn_import_csv.clicked.connect(self.import_csv_to_db)
        self.btn_import_csv.setEnabled(False)
        ctrl_layout.addWidget(self.btn_import_csv)
        layout.addLayout(ctrl_layout)
        
        self.table_csv_preview = QTableWidget()
        self.table_csv_preview.setStyleSheet("background-color: #1e1e1e;")
        layout.addWidget(self.table_csv_preview)

    def setup_tab3_browser(self):
        layout = QVBoxLayout(self.tab3_browser)
        filter_layout = QHBoxLayout()
        filter_layout.addWidget(QLabel("テーブル:"))
        self.combo_tables = QComboBox()
        self.combo_tables.currentTextChanged.connect(self.on_table_changed)
        filter_layout.addWidget(self.combo_tables)
        
        self.btn_refresh_data = QPushButton("🔄 更新")
        self.btn_refresh_data.clicked.connect(self.reload_browser_data)
        filter_layout.addWidget(self.btn_refresh_data)
        filter_layout.addStretch()
        
        self.btn_prev_page = QPushButton("◀ 前の500件")
        self.btn_prev_page.clicked.connect(self.prev_page)
        self.lbl_page_info = QLabel("ページ: 1 / 1")
        self.btn_next_page = QPushButton("次の500件 ▶")
        self.btn_next_page.clicked.connect(self.next_page)
        filter_layout.addWidget(self.btn_prev_page)
        filter_layout.addWidget(self.lbl_page_info)
        filter_layout.addWidget(self.btn_next_page)
        layout.addLayout(filter_layout)
        
        lbl_hint = QLabel("💡 Hint: 列の見出し(ヘッダー)を右クリックすると、エクセルの「▽」のようなフィルター絞り込みができます。")
        lbl_hint.setStyleSheet("color: #f39c12; font-weight: bold;")
        layout.addWidget(lbl_hint)
        
        self.table_browser = QTableWidget()
        self.table_browser.setStyleSheet("background-color: #1e1e1e; gridline-color: #444; color: #e0e0e0;")
        self.table_browser.cellChanged.connect(self.on_cell_edited)
        
        header = self.table_browser.horizontalHeader()
        header.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        header.customContextMenuRequested.connect(self.show_header_menu)
        
        layout.addWidget(self.table_browser)

    def show_header_menu(self, pos):
        col = self.table_browser.horizontalHeader().logicalIndexAt(pos)
        if col < 0: return
        col_name = self.table_browser.horizontalHeaderItem(col).text()

        menu = QMenu(self)
        menu.setStyleSheet("QMenu { background-color: #2b2b2b; color: white; border: 1px solid #555; } QMenu::item:selected { background-color: #2980b9; }")
        
        action_filter = QAction(f"🔍 '{col_name}' で絞り込み", self)
        action_filter.triggered.connect(lambda: self.open_filter_dialog(col_name))
        menu.addAction(action_filter)

        if col_name in self.active_filters:
            action_clear = QAction(f"❌ '{col_name}' のフィルターを解除", self)
            action_clear.triggered.connect(lambda: self.clear_filter(col_name))
            menu.addAction(action_clear)
            
        if self.active_filters:
            action_clear_all = QAction("🧹 全てのフィルターを解除", self)
            action_clear_all.triggered.connect(self.clear_all_filters)
            menu.addAction(action_clear_all)

        menu.exec(self.table_browser.horizontalHeader().mapToGlobal(pos))

    def open_filter_dialog(self, col_name):
        dialog = FilterDialog(col_name, self)
        if dialog.exec():
            cond = dialog.get_condition()
            if cond:
                self.active_filters[col_name] = cond
                self.current_page = 1
                self.reload_browser_data()

    def clear_filter(self, col_name):
        if col_name in self.active_filters:
            del self.active_filters[col_name]
            self.current_page = 1
            self.reload_browser_data()
            
    def clear_all_filters(self):
        self.active_filters.clear()
        self.current_page = 1
        self.reload_browser_data()

    def select_db_file(self):
        path, _ = QFileDialog.getSaveFileName(self, "DBファイルを選択/作成", self.current_db_path, "DB (*.db *.sqlite)")
        if path:
            self.current_db_path = path
            self.lbl_db_path.setText(f"現在のDB: {self.current_db_path}")
            self.update_browser_tables()

    def select_npz_dir(self):
        d = QFileDialog.getExistingDirectory(self, "NPZフォルダを選択", self.current_npz_dir)
        if d:
            self.current_npz_dir = d
            self.lbl_npz_dir.setText(f"NPZ: {self.current_npz_dir}")

    def start_db_generation(self):
        self.btn_generate_db.setEnabled(False)
        self.progress_bar.setVisible(True)
        self.progress_bar.setValue(0)
        self.db_thread = DBGeneratorThread(self.current_npz_dir, self.current_db_path)
        self.db_thread.progress.connect(self.update_progress)
        self.db_thread.finished.connect(self.on_db_generated)
        self.db_thread.error.connect(self.on_db_error)
        self.db_thread.start()

    def update_progress(self, val, text):
        self.progress_bar.setValue(val)
        self.lbl_status.setText(text)

    def on_db_generated(self, msg):
        self.btn_generate_db.setEnabled(True)
        self.progress_bar.setVisible(False)
        self.lbl_status.setText(msg)
        QMessageBox.information(self, "生成完了", msg)
        self.update_browser_tables()

    def on_db_error(self, msg):
        self.btn_generate_db.setEnabled(True)
        self.progress_bar.setVisible(False)
        QMessageBox.critical(self, "エラー", msg)

    def preview_csv(self):
        path, _ = QFileDialog.getOpenFileName(self, "観測データCSVを開く", "", "CSV (*.csv)")
        if not path: return
        try:
            self.current_csv_data = pd.read_csv(path)
            df = self.current_csv_data
            self.table_csv_preview.setColumnCount(len(df.columns))
            self.table_csv_preview.setHorizontalHeaderLabels(df.columns)
            self.table_csv_preview.setRowCount(min(len(df), 100))
            for r in range(min(len(df), 100)):
                for c in range(len(df.columns)):
                    self.table_csv_preview.setItem(r, c, QTableWidgetItem(str(df.iat[r, c])))
            self.btn_import_csv.setEnabled(True)
        except Exception as e:
            QMessageBox.critical(self, "読込エラー", str(e))

    def import_csv_to_db(self):
        if self.current_csv_data is None: return
        t_name = self.txt_table_name.text().strip()
        if not t_name: return
        
        conn = None
        try:
            conn = sqlite3.connect(self.current_db_path, timeout=15)
            self.current_csv_data.to_sql(t_name, conn, if_exists='append', index=False)
            QMessageBox.information(self, "完了", f"'{t_name}' に {len(self.current_csv_data)} 件インポートしました。")
            self.update_browser_tables()
        except Exception as e:
            QMessageBox.critical(self, "DB書込エラー", str(e))
        finally:
            if conn:
                conn.close() 

    def update_browser_tables(self):
        self.combo_tables.blockSignals(True)
        self.combo_tables.clear()
        conn = None
        try:
            conn = sqlite3.connect(self.current_db_path, timeout=15)
            cursor = conn.cursor()
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
            tables = [r[0] for r in cursor.fetchall() if r[0] != "sqlite_sequence"]
            self.combo_tables.addItems(tables)
        except: pass
        finally:
            if conn: conn.close()
            
        self.combo_tables.blockSignals(False)
        if hasattr(self, 'combo_tables') and self.combo_tables.count() > 0:
            self.current_page = 1
            self.active_filters.clear()
            self.reload_browser_data()

    def on_table_changed(self, text):
        self.current_page = 1
        self.active_filters.clear()
        self.reload_browser_data()

    def prev_page(self):
        if self.current_page > 1: self.current_page -= 1; self.reload_browser_data()
    def next_page(self):
        max_p = max(1, (self.total_records + self.records_per_page - 1) // self.records_per_page)
        if self.current_page < max_p: self.current_page += 1; self.reload_browser_data()

    def reload_browser_data(self):
        table_name = self.combo_tables.currentText()
        if not table_name or not os.path.exists(self.current_db_path): return
        
        self.is_loading_table = True
        self.table_browser.clear()
        
        conn = None
        try:
            conn = sqlite3.connect(self.current_db_path, timeout=15)
            cursor = conn.cursor()
            
            where_clauses = [f"{col} {cond}" for col, cond in self.active_filters.items()]
            where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
            
            cursor.execute(f"SELECT COUNT(*) FROM {table_name}{where_sql}")
            self.total_records = cursor.fetchone()[0]
            max_pages = max(1, (self.total_records + self.records_per_page - 1) // self.records_per_page)
            if self.current_page > max_pages: self.current_page = max_pages
            
            offset = (self.current_page - 1) * self.records_per_page
            cursor.execute(f"SELECT rowid, * FROM {table_name}{where_sql} LIMIT {self.records_per_page} OFFSET {offset}")
            rows = cursor.fetchall()
            
            col_names = [desc[0] for desc in cursor.description]
            self.table_browser.setColumnCount(len(col_names))
            
            header_labels = []
            for c in col_names:
                header_labels.append(f"🔍 {c}" if c in self.active_filters else c)
                
            self.table_browser.setHorizontalHeaderLabels(header_labels)
            self.table_browser.setRowCount(len(rows))
            
            for r_idx, row in enumerate(rows):
                for c_idx, val in enumerate(row):
                    item = QTableWidgetItem("" if val is None else str(val))
                    if c_idx == 0: item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
                    self.table_browser.setItem(r_idx, c_idx, item)
            
            filter_msg = f" (フィルター適用中)" if self.active_filters else ""
            self.lbl_page_info.setText(f"ページ: {self.current_page} / {max_pages} {filter_msg}")
            self.btn_prev_page.setEnabled(self.current_page > 1)
            self.btn_next_page.setEnabled(self.current_page < max_pages)
            
        except Exception as e:
            print("Browser Load Error:", e)
        finally:
            if conn: conn.close()
            self.is_loading_table = False

    def on_cell_edited(self, row, col):
        if self.is_loading_table: return
        
        table_name = self.combo_tables.currentText()
        col_name = self.table_browser.horizontalHeaderItem(col).text().replace("🔍 ", "")
        rowid_item = self.table_browser.item(row, 0)
        new_val_item = self.table_browser.item(row, col)
        
        if not rowid_item or not new_val_item: return
        row_id = rowid_item.text()
        new_val = new_val_item.text()
        
        conn = None
        try:
            conn = sqlite3.connect(self.current_db_path, timeout=15)
            cursor = conn.cursor()
            cursor.execute(f"UPDATE {table_name} SET {col_name} = ? WHERE rowid = ?", (new_val, row_id))
            conn.commit()
        except Exception as e:
            QMessageBox.critical(self, "UPDATE エラー", str(e))
            self.reload_browser_data()
        finally:
            if conn: conn.close()

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

コメント