未分類

import sys
import os
import sqlite3
import requests
import pandas as pd
from io import StringIO
from datetime import datetime, timedelta
import time

from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
    QPushButton, QTextEdit, QTabWidget, QComboBox, QDateEdit, 
    QLabel, QTableWidget, QTableWidgetItem, QCheckBox, QGroupBox, QMessageBox,
    QFileDialog, QLineEdit
)
from PyQt6.QtCore import QThread, pyqtSignal, QDate, Qt

# ==========================================
# 取得可能な地点設定 (必要に応じて追加可能)
# ==========================================
LOCATIONS = [
    {"name": "札幌", "prec_no": "14", "block_no": "47412", "upper_point": "47412"},
    {"name": "東京", "prec_no": "44", "block_no": "47662", "upper_point": "47662"},
    {"name": "仙台", "prec_no": "34", "block_no": "47590", "upper_point": "47590"},
    {"name": "大阪", "prec_no": "62", "block_no": "47772", "upper_point": None},
    {"name": "福岡", "prec_no": "82", "block_no": "47807", "upper_point": "47807"},
    {"name": "那覇", "prec_no": "91", "block_no": "47936", "upper_point": "47936"}
]
TARGET_PRESSURES = {1000, 925, 900, 850, 800, 700, 600, 500, 400, 350, 300, 250, 200, 175, 150, 125, 100}

def get_next_month(d):
    return (d.replace(day=28) + timedelta(days=4)).replace(day=1)

def get_prev_month(d):
    return (d.replace(day=1) - timedelta(days=1)).replace(day=1)

# ==========================================
# データ取得用スレッド
# ==========================================
class ScraperThread(QThread):
    log_signal = pyqtSignal(str)
    finished_signal = pyqtSignal()

    def __init__(self, targets, location, db_path):
        super().__init__()
        self.targets = targets
        self.loc = location
        self.db_path = db_path
        self.is_running = True 

    def stop(self):
        self.is_running = False

    def run(self):
        self.log_signal.emit(f"データベース接続中... ({os.path.basename(self.db_path)})")
        try:
            conn = sqlite3.connect(self.db_path)
            self.setup_database(conn)
            
            session = requests.Session()
            session.headers.update({"User-Agent": "Mozilla/5.0"})
            
            if self.is_running and self.targets.get("daily"):
                self.process_daily_hybrid(conn, session, self.loc)
            if self.is_running and self.targets.get("hourly"):
                self.process_hourly_hybrid(conn, session, self.loc)
            if self.is_running and self.targets.get("min10"):
                self.process_10min_hybrid(conn, session, self.loc)
            if self.is_running and self.targets.get("upper"):
                self.process_upper_hybrid(conn, session, self.loc)

        except Exception as e:
            self.log_signal.emit(f"エラー発生: {e}")
        finally:
            if 'conn' in locals():
                conn.close()
            if not self.is_running:
                self.log_signal.emit("\n■ ユーザー操作により処理を中断しました。")
            else:
                self.log_signal.emit("\n■ 全処理が完了・または遡り限界に到達しました。")
            self.finished_signal.emit()

    def setup_database(self, conn):
        conn.execute("CREATE TABLE IF NOT EXISTS Amedas10Min(PointCode TEXT, ObsDate TEXT, ObsTime TEXT, Pressure REAL, SeaPressure REAL, Rain REAL, Rain1H REAL, Rain3H REAL, Rain6H REAL, Rain12H REAL, Rain24H REAL, Temperature REAL, Humidity REAL, WindSpeed REAL, WindDirection TEXT, MaxWind REAL, MaxWindDirection TEXT, Sunshine REAL, PRIMARY KEY(PointCode, ObsDate, ObsTime))")
        conn.execute("CREATE TABLE IF NOT EXISTS AmedasHourly(PointCode TEXT, ObsDate TEXT, ObsHour TEXT, Pressure REAL, SeaPressure REAL, Rain REAL, Rain3H REAL, Rain6H REAL, Rain12H REAL, Rain24H REAL, Temperature REAL, DewPoint REAL, VaporPressure REAL, Humidity REAL, WindSpeed REAL, WindDirection TEXT, Sunshine REAL, SolarRadiation REAL, Snowfall REAL, Snow12H REAL, Snow24H REAL, SnowDepth REAL, Weather TEXT, Visibility REAL, PRIMARY KEY(PointCode, ObsDate, ObsHour))")
        conn.execute("CREATE TABLE IF NOT EXISTS UpperAir(PointCode TEXT, ObsDay TEXT, ObsTime TEXT, Pressure INTEGER, Height REAL, Temperature REAL, Humidity REAL, WindSpeed REAL, WindDirection REAL, PRIMARY KEY(PointCode, ObsDay, ObsTime, Pressure))")
        conn.execute("""CREATE TABLE IF NOT EXISTS AmedasDaily(
            PointCode TEXT, ObsDate TEXT, PressAvg REAL, SeaPressAvg REAL, SeaPressMin REAL, SeaPressMinTime TEXT, RainTot REAL, Rain1HMax REAL, Rain1HMaxTime TEXT, Rain10MMax REAL, Rain10MMaxTime TEXT,
            TempAvg REAL, TempMax REAL, TempMaxTime TEXT, TempMin REAL, TempMinTime TEXT, VaporAvg REAL, HumAvg REAL, HumMin REAL, HumMinTime TEXT, WindAvg REAL, WindMax REAL, WindMaxDir TEXT, WindMaxTime TEXT, 
            GustMax REAL, GustMaxDir TEXT, GustMaxTime TEXT, WindFreqDir TEXT, Sunshine REAL, SolarRad REAL, Snowfall REAL, SnowDepth REAL, CloudCover REAL, DayWeather TEXT, NightWeather TEXT, 
            Rain3HMax REAL, Rain3HMaxTime TEXT, Rain6HMax REAL, Rain6HMaxTime TEXT, Rain12HMax REAL, Rain12HMaxTime TEXT, Rain24HMax REAL, Rain24HMaxTime TEXT, Rain48HMax REAL, Rain48HMaxTime TEXT, Rain72HMax REAL, Rain72HMaxTime TEXT,
            Snow3HMax REAL, Snow3HMaxTime TEXT, Snow6HMax REAL, Snow6HMaxTime TEXT, Snow12HMax REAL, Snow12HMaxTime TEXT, Snow24HMax REAL, Snow24HMaxTime TEXT, Snow48HMax REAL, Snow48HMaxTime TEXT, Snow72HMax REAL, Snow72HMaxTime TEXT,
            PRIMARY KEY(PointCode, ObsDate)
        )""")
        conn.commit()

    def get_date_range(self, conn, table_name, point_code):
        cur = conn.cursor()
        date_col = "ObsDay" if table_name == "UpperAir" else "ObsDate"
        cur.execute(f"SELECT MIN({date_col}), MAX({date_col}) FROM {table_name} WHERE PointCode = ?", (point_code,))
        return cur.fetchone()

    def fetch_tables(self, session, url):
        try:
            r = session.get(url, timeout=30)
            r.encoding = r.apparent_encoding # 文字化け防止対策
            if "ページを表示することが出来ませんでした" in r.text or r.status_code != 200: return None
            return pd.read_html(StringIO(r.text))
        except Exception: 
            return None
            
    def clean_dataframe(self, df):
        df.replace("--", None, inplace=True)
        df.replace("///", None, inplace=True)
        df.replace("×", None, inplace=True)
        return df.where(pd.notnull(df), None)

    def fix_nhour_shift(self, df):
        df.columns = range(df.shape[1])
        if not df.empty and str(df.iloc[0, 0]) == '日':
            row_vals = df.iloc[0].astype(str).tolist()
            corrected_row = ['1'] + row_vals[2:] + [None]
            for i, val in enumerate(corrected_row[:len(df.columns)]):
                df.iat[0, i] = val
        df['Day'] = pd.to_numeric(df.get(0), errors='coerce')
        return df.dropna(subset=['Day']).copy()

    # --- 日別値 ---
    def process_daily_hybrid(self, conn, session, loc):
        min_date, max_date = self.get_date_range(conn, "AmedasDaily", loc["block_no"])
        two_days_ago = datetime.now() - timedelta(days=2)
        
        start_fwd = datetime.strptime(max_date, "%Y-%m-%d").replace(day=1) if max_date else two_days_ago.replace(day=1)
        self.log_signal.emit(f"\n--- {loc['name']} 日別値 [最新更新] ---")
        self._fetch_daily_loop(conn, session, loc, start_fwd, two_days_ago, direction="forward")

        if not self.is_running: return

        start_bwd = get_prev_month(datetime.strptime(min_date, "%Y-%m-%d")) if min_date else get_prev_month(two_days_ago)
        self.log_signal.emit(f"\n--- {loc['name']} 日別値 [過去遡り] ---")
        self._fetch_daily_loop(conn, session, loc, start_bwd, None, direction="backward")

    def _fetch_daily_loop(self, conn, session, loc, current, end_date, direction):
        base_url = f"https://www.data.jma.go.jp/stats/etrn/view/daily_s1.php?prec_no={loc['prec_no']}&block_no={loc['block_no']}&year={{}}&month={{}}&day=&view={{}}"
        
        while self.is_running:
            if direction == "forward" and current.strftime("%Y-%m") > end_date.strftime("%Y-%m"):
                break
                
            year, month = current.year, current.month
            tb_a1 = self.fetch_tables(session, base_url.format(year, month, "a1"))
            
            if not tb_a1 or len(tb_a1) == 0:
                if direction == "backward":
                    self.log_signal.emit(f"データなし: {year}年{month}月 (日別値の遡り限界に到達しました)")
                    break
                else:
                    current = get_next_month(current)
                    continue

            df_a1 = self.clean_dataframe(tb_a1[0].copy())
            df_a1.columns = range(df_a1.shape[1])
            df_a1['Day'] = pd.to_numeric(df_a1.get(0), errors='coerce')
            df_a1 = df_a1.dropna(subset=['Day'])
            
            # .get()を使用し、過去データで列が足りない場合でも安全に回避
            res_a1 = pd.DataFrame({'Day': df_a1['Day'], 'PressAvg': df_a1.get(1), 'SeaPressAvg': df_a1.get(2), 'SeaPressMin': df_a1.get(3), 'SeaPressMinTime': df_a1.get(4), 'RainTot': df_a1.get(5), 'Rain1HMax': df_a1.get(6), 'Rain1HMaxTime': df_a1.get(7), 'Rain10MMax': df_a1.get(8), 'Rain10MMaxTime': df_a1.get(9), 'TempAvg': df_a1.get(10), 'TempMax': df_a1.get(11), 'TempMaxTime': df_a1.get(12), 'TempMin': df_a1.get(13), 'TempMinTime': df_a1.get(14), 'VaporAvg': df_a1.get(15), 'HumAvg': df_a1.get(16), 'HumMin': df_a1.get(17), 'HumMinTime': df_a1.get(18)})
            
            tb_a3 = self.fetch_tables(session, base_url.format(year, month, "a3"))
            merged = res_a1
            if tb_a3 and len(tb_a3) > 0:
                df_a3 = self.clean_dataframe(tb_a3[0].copy())
                df_a3.columns = range(df_a3.shape[1])
                df_a3['Day'] = pd.to_numeric(df_a3.get(0), errors='coerce')
                res_a3 = pd.DataFrame({'Day': df_a3['Day'], 'WindAvg': df_a3.get(1), 'WindMax': df_a3.get(2), 'WindMaxDir': df_a3.get(3), 'WindMaxTime': df_a3.get(4), 'GustMax': df_a3.get(5), 'GustMaxDir': df_a3.get(6), 'GustMaxTime': df_a3.get(7), 'WindFreqDir': df_a3.get(8), 'Sunshine': df_a3.get(9), 'SolarRad': df_a3.get(10), 'Snowfall': df_a3.get(11), 'SnowDepth': df_a3.get(12), 'CloudCover': df_a3.get(13), 'DayWeather': df_a3.get(14), 'NightWeather': df_a3.get(15)})
                merged = merged.merge(res_a3, on='Day', how='left')

            tb_a5 = self.fetch_tables(session, base_url.format(year, month, "a5"))
            if tb_a5 and len(tb_a5) > 0:
                df_a5 = self.fix_nhour_shift(self.clean_dataframe(tb_a5[0].copy()))
                res_a5 = pd.DataFrame({'Day': df_a5['Day'], 'Rain3HMax': df_a5.get(3), 'Rain3HMaxTime': df_a5.get(4), 'Rain6HMax': df_a5.get(5), 'Rain6HMaxTime': df_a5.get(6), 'Rain12HMax': df_a5.get(7), 'Rain12HMaxTime': df_a5.get(8), 'Rain24HMax': df_a5.get(9), 'Rain24HMaxTime': df_a5.get(10), 'Rain48HMax': df_a5.get(11), 'Rain48HMaxTime': df_a5.get(12), 'Rain72HMax': df_a5.get(13), 'Rain72HMaxTime': df_a5.get(14)})
                merged = merged.merge(res_a5, on='Day', how='left')

            tb_a6 = self.fetch_tables(session, base_url.format(year, month, "a6"))
            if tb_a6 and len(tb_a6) > 0:
                df_a6 = self.fix_nhour_shift(self.clean_dataframe(tb_a6[0].copy()))
                res_a6 = pd.DataFrame({'Day': df_a6['Day'], 'Snow3HMax': df_a6.get(1), 'Snow3HMaxTime': df_a6.get(2), 'Snow6HMax': df_a6.get(3), 'Snow6HMaxTime': df_a6.get(4), 'Snow12HMax': df_a6.get(5), 'Snow12HMaxTime': df_a6.get(6), 'Snow24HMax': df_a6.get(7), 'Snow24HMaxTime': df_a6.get(8), 'Snow48HMax': df_a6.get(9), 'Snow48HMaxTime': df_a6.get(10), 'Snow72HMax': df_a6.get(11), 'Snow72HMaxTime': df_a6.get(12)})
                merged = merged.merge(res_a6, on='Day', how='left')

            expected_cols = ['PressAvg', 'SeaPressAvg', 'SeaPressMin', 'SeaPressMinTime', 'RainTot', 'Rain1HMax', 'Rain1HMaxTime', 'Rain10MMax', 'Rain10MMaxTime', 'TempAvg', 'TempMax', 'TempMaxTime', 'TempMin', 'TempMinTime', 'VaporAvg', 'HumAvg', 'HumMin', 'HumMinTime', 'WindAvg', 'WindMax', 'WindMaxDir', 'WindMaxTime', 'GustMax', 'GustMaxDir', 'GustMaxTime', 'WindFreqDir', 'Sunshine', 'SolarRad', 'Snowfall', 'SnowDepth', 'CloudCover', 'DayWeather', 'NightWeather', 'Rain3HMax', 'Rain3HMaxTime', 'Rain6HMax', 'Rain6HMaxTime', 'Rain12HMax', 'Rain12HMaxTime', 'Rain24HMax', 'Rain24HMaxTime', 'Rain48HMax', 'Rain48HMaxTime', 'Rain72HMax', 'Rain72HMaxTime', 'Snow3HMax', 'Snow3HMaxTime', 'Snow6HMax', 'Snow6HMaxTime', 'Snow12HMax', 'Snow12HMaxTime', 'Snow24HMax', 'Snow24HMaxTime', 'Snow48HMax', 'Snow48HMaxTime', 'Snow72HMax', 'Snow72HMaxTime']
            for c in expected_cols:
                if c not in merged.columns: merged[c] = None

            records = []
            for _, row in merged.iterrows():
                try:
                    obs_date = f"{year:04d}-{month:02d}-{int(row['Day']):02d}"
                    records.append(tuple([loc["block_no"], obs_date] + [row.get(c) for c in expected_cols]))
                except ValueError: pass

            conn.executemany(f"INSERT OR REPLACE INTO AmedasDaily VALUES({','.join(['?']*58)})", records)
            conn.commit()
            
            self.log_signal.emit(f"保存完了: {year}-{month:02d} ({len(records)}件)")
            time.sleep(1)
            
            current = get_next_month(current) if direction == "forward" else get_prev_month(current)


    # --- 1時間値 ---
    def process_hourly_hybrid(self, conn, session, loc):
        min_date, max_date = self.get_date_range(conn, "AmedasHourly", loc["block_no"])
        two_days_ago = datetime.now() - timedelta(days=2)
        
        start_fwd = datetime.strptime(max_date, "%Y-%m-%d") if max_date else two_days_ago
        self.log_signal.emit(f"\n--- {loc['name']} 1時間値 [最新更新] ---")
        self._fetch_generic_loop(conn, session, loc, start_fwd, two_days_ago, "AmedasHourly", "forward")
        
        if not self.is_running: return

        start_bwd = datetime.strptime(min_date, "%Y-%m-%d") - timedelta(days=1) if min_date else two_days_ago - timedelta(days=1)
        self.log_signal.emit(f"\n--- {loc['name']} 1時間値 [過去遡り] ---")
        self._fetch_generic_loop(conn, session, loc, start_bwd, None, "AmedasHourly", "backward")

    # --- 10分値 ---
    def process_10min_hybrid(self, conn, session, loc):
        min_date, max_date = self.get_date_range(conn, "Amedas10Min", loc["block_no"])
        two_days_ago = datetime.now() - timedelta(days=2)
        
        start_fwd = datetime.strptime(max_date, "%Y-%m-%d") if max_date else two_days_ago
        self.log_signal.emit(f"\n--- {loc['name']} 10分値 [最新更新] ---")
        self._fetch_generic_loop(conn, session, loc, start_fwd, two_days_ago, "Amedas10Min", "forward")

        if not self.is_running: return

        start_bwd = datetime.strptime(min_date, "%Y-%m-%d") - timedelta(days=1) if min_date else two_days_ago - timedelta(days=1)
        self.log_signal.emit(f"\n--- {loc['name']} 10分値 [過去遡り] ---")
        self._fetch_generic_loop(conn, session, loc, start_bwd, None, "Amedas10Min", "backward")

    # --- 高層データ ---
    def process_upper_hybrid(self, conn, session, loc):
        if not loc.get("upper_point"):
            self.log_signal.emit(f"\n※ {loc['name']} は高層データ非対応地点のためスキップします。")
            return
        
        min_date, max_date = self.get_date_range(conn, "UpperAir", loc["upper_point"])
        two_days_ago = datetime.now() - timedelta(days=2)
        
        start_fwd = datetime.strptime(max_date, "%Y-%m-%d") if max_date else two_days_ago
        self.log_signal.emit(f"\n--- {loc['name']} 高層データ [最新更新] ---")
        self._fetch_generic_loop(conn, session, loc, start_fwd, two_days_ago, "UpperAir", "forward")

        if not self.is_running: return

        start_bwd = datetime.strptime(min_date, "%Y-%m-%d") - timedelta(days=1) if min_date else two_days_ago - timedelta(days=1)
        self.log_signal.emit(f"\n--- {loc['name']} 高層データ [過去遡り] ---")
        self._fetch_generic_loop(conn, session, loc, start_bwd, None, "UpperAir", "backward")

    # 汎用ループ
    def _fetch_generic_loop(self, conn, session, loc, current, end_date, table_name, direction):
        missing_count = 0
        while self.is_running:
            if direction == "forward" and current.strftime("%Y-%m-%d") > end_date.strftime("%Y-%m-%d"): break
            if missing_count >= 3 and direction == "backward":
                self.log_signal.emit(f"3日連続データなし: {table_name}の遡り限界に到達しました。")
                break

            url = ""
            if table_name == "AmedasHourly": url = f"https://www.data.jma.go.jp/stats/etrn/view/hourly_s1.php?prec_no={loc['prec_no']}&block_no={loc['block_no']}&year={current.year}&month={current.month}&day={current.day}&view=p1"
            elif table_name == "Amedas10Min": url = f"https://www.data.jma.go.jp/stats/etrn/view/10min_s1.php?prec_no={loc['prec_no']}&block_no={loc['block_no']}&year={current.year}&month={current.month}&day={current.day}&view="
            elif table_name == "UpperAir": url_template = f"https://www.data.jma.go.jp/stats/etrn/upper/view/hourly_usp.php?year={current.year}&month={current.month}&day={current.day}&hour={{}}&atm=&point={loc['upper_point']}&view="

            records = []
            obs_date = current.strftime("%Y-%m-%d")

            if table_name in ["AmedasHourly", "Amedas10Min"]:
                tables = self.fetch_tables(session, url)
                if tables and len(tables) > 0:
                    df = self.clean_dataframe(tables[0].copy())
                    df.columns = range(df.shape[1])
                    if table_name == "AmedasHourly":
                        for _, row in df.iterrows():
                            records.append((
                                loc["block_no"], obs_date, row.get(0), row.get(1), row.get(2), row.get(3), 
                                None, None, None, None, row.get(4), row.get(5), row.get(6), row.get(7), 
                                row.get(8), row.get(9), row.get(10), row.get(11), row.get(12), None, 
                                None, row.get(13), str(row.get(14)) if pd.notnull(row.get(14)) else None, row.get(16)
                            ))
                    elif table_name == "Amedas10Min":
                        for _, row in df.iterrows():
                            records.append((
                                loc["block_no"], obs_date, row.get(0), row.get(1), row.get(2), row.get(3), 
                                None, None, None, None, None, row.get(4), row.get(5), row.get(6), 
                                row.get(7), row.get(8), row.get(9), row.get(10)
                            ))
            elif table_name == "UpperAir":
                for hour in [9, 21]:
                    tables = self.fetch_tables(session, url_template.format(hour))
                    if tables and len(tables) >= 2:
                        df = self.clean_dataframe(tables[1].copy())
                        df.columns = range(df.shape[1])
                        df.iloc[:, 0] = pd.to_numeric(df.iloc[:, 0], errors="coerce")
                        df = df[df.iloc[:, 0].isin(TARGET_PRESSURES)]
                        for _, row in df.iterrows():
                            records.append((
                                loc["upper_point"], obs_date, f"{hour:02d}:00", 
                                int(row.get(0)) if pd.notnull(row.get(0)) else None, 
                                row.get(1), row.get(2), row.get(3), row.get(4), row.get(5)
                            ))

            if records:
                missing_count = 0
                placeholders = ",".join(["?"] * len(records[0]))
                conn.executemany(f"INSERT OR REPLACE INTO {table_name} VALUES({placeholders})", records)
                conn.commit()
                self.log_signal.emit(f"保存完了: {obs_date} ({len(records)}件)")
            else:
                missing_count += 1
                self.log_signal.emit(f"データなし: {obs_date}")

            time.sleep(1)
            current = current + timedelta(days=1) if direction == "forward" else current - timedelta(days=1)

# ==========================================
# GUI メインウィンドウ
# ==========================================
class WeatherApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("気象データ管理・閲覧・分析ツール")
        self.resize(1000, 750)
        
        main_widget = QWidget()
        main_layout = QVBoxLayout()
        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)

        db_layout = QHBoxLayout()
        self.db_path_edit = QLineEdit("weather_data.db")
        self.db_path_edit.setReadOnly(True)
        btn_db_select = QPushButton("データベース選択 / 新規作成")
        btn_db_select.clicked.connect(self.select_database)
        db_layout.addWidget(QLabel("保存・参照DBファイル:"))
        db_layout.addWidget(self.db_path_edit)
        db_layout.addWidget(btn_db_select)
        
        main_layout.addLayout(db_layout)

        self.tabs = QTabWidget()
        main_layout.addWidget(self.tabs)
        
        self.setup_scraper_tab()
        self.setup_viewer_tab()

    def select_database(self):
        path, _ = QFileDialog.getSaveFileName(self, "データベースファイルを選択または作成", "", "SQLite Database (*.db);;All Files (*)")
        if path:
            self.db_path_edit.setText(path)

    def setup_scraper_tab(self):
        tab = QWidget()
        layout = QVBoxLayout()
        
        group_box = QGroupBox("取得対象の設定")
        group_layout = QVBoxLayout()
        
        loc_layout = QHBoxLayout()
        self.cb_scrape_loc = QComboBox()
        for loc in LOCATIONS:
            self.cb_scrape_loc.addItem(loc["name"], userData=loc)
        loc_layout.addWidget(QLabel("取得する地点:"))
        loc_layout.addWidget(self.cb_scrape_loc)
        loc_layout.addStretch()
        
        chk_layout = QHBoxLayout()
        self.chk_daily = QCheckBox("日別値")
        self.chk_hourly = QCheckBox("1時間値")
        self.chk_10min = QCheckBox("10分値")
        self.chk_upper = QCheckBox("高層データ")
        self.chk_daily.setChecked(True)
        chk_layout.addWidget(self.chk_daily)
        chk_layout.addWidget(self.chk_hourly)
        chk_layout.addWidget(self.chk_10min)
        chk_layout.addWidget(self.chk_upper)
        
        group_layout.addLayout(loc_layout)
        group_layout.addLayout(chk_layout)
        group_box.setLayout(group_layout)

        btn_layout = QHBoxLayout()
        self.btn_start = QPushButton("データ取得・過去への遡り開始")
        self.btn_start.clicked.connect(self.start_scraping)
        self.btn_start.setMinimumHeight(40)
        
        self.btn_stop = QPushButton("停止")
        self.btn_stop.clicked.connect(self.stop_scraping)
        self.btn_stop.setMinimumHeight(40)
        self.btn_stop.setEnabled(False)
        self.btn_stop.setStyleSheet("background-color: #ffcdd2;")

        btn_layout.addWidget(self.btn_start)
        btn_layout.addWidget(self.btn_stop)
        
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)

        layout.addWidget(group_box)
        layout.addLayout(btn_layout)
        layout.addWidget(QLabel("実行ログ:"))
        layout.addWidget(self.log_text)
        tab.setLayout(layout)
        self.tabs.addTab(tab, "データ取得")

    def setup_viewer_tab(self):
        tab = QWidget()
        layout = QVBoxLayout()

        control_layout = QHBoxLayout()
        self.cb_location = QComboBox()
        for loc in LOCATIONS: self.cb_location.addItem(loc["name"], userData=loc["block_no"])
        self.cb_table = QComboBox()
        self.cb_table.addItems(["AmedasDaily", "AmedasHourly", "Amedas10Min", "UpperAir"])

        self.date_start = QDateEdit()
        self.date_start.setCalendarPopup(True)
        self.date_start.setDate(QDate.currentDate().addYears(-1))
        
        self.date_end = QDateEdit()
        self.date_end.setCalendarPopup(True)
        self.date_end.setDate(QDate.currentDate())

        control_layout.addWidget(QLabel("地点:"))
        control_layout.addWidget(self.cb_location)
        control_layout.addWidget(QLabel("テーブル:"))
        control_layout.addWidget(self.cb_table)
        control_layout.addWidget(QLabel("期間:"))
        control_layout.addWidget(self.date_start)
        control_layout.addWidget(QLabel("〜"))
        control_layout.addWidget(self.date_end)
        
        btn_layout = QHBoxLayout()
        self.btn_search = QPushButton("生データを検索")
        self.btn_search.clicked.connect(self.search_raw_data)
        self.btn_analyze = QPushButton("期間統計を分析")
        self.btn_analyze.clicked.connect(self.analyze_data)
        self.btn_analyze.setStyleSheet("background-color: #e0f7fa; font-weight: bold;")
        btn_layout.addWidget(self.btn_search)
        btn_layout.addWidget(self.btn_analyze)

        self.table_widget = QTableWidget()
        layout.addLayout(control_layout)
        layout.addLayout(btn_layout)
        layout.addWidget(self.table_widget)
        tab.setLayout(layout)
        self.tabs.addTab(tab, "データ閲覧・分析")

    def start_scraping(self):
        targets = {"daily": self.chk_daily.isChecked(), "hourly": self.chk_hourly.isChecked(), "min10": self.chk_10min.isChecked(), "upper": self.chk_upper.isChecked()}
        if not any(targets.values()): return QMessageBox.warning(self, "警告", "取得するデータ種別を選択してください。")
        
        target_loc = self.cb_scrape_loc.currentData()
        db_path = self.db_path_edit.text()

        self.btn_start.setEnabled(False)
        self.btn_stop.setEnabled(True)
        self.log_text.clear()
        
        self.thread = ScraperThread(targets, target_loc, db_path)
        self.thread.log_signal.connect(self.append_log)
        self.thread.finished_signal.connect(self.scraping_finished)
        self.thread.start()

    def stop_scraping(self):
        if hasattr(self, 'thread') and self.thread.isRunning():
            self.append_log("停止リクエストを送信しました。現在の処理が終わり次第終了します...")
            self.thread.stop()
            self.btn_stop.setEnabled(False)

    def append_log(self, msg):
        self.log_text.append(msg)
        scrollbar = self.log_text.verticalScrollBar()
        scrollbar.setValue(scrollbar.maximum())

    def scraping_finished(self):
        self.btn_start.setEnabled(True)
        self.btn_stop.setEnabled(False)
        QMessageBox.information(self, "完了", "データ取得プロセスが終了しました。")

    def search_raw_data(self):
        self._load_table_data(is_analysis=False)

    def analyze_data(self):
        self._load_table_data(is_analysis=True)

    def _load_table_data(self, is_analysis):
        db_path = self.db_path_edit.text()
        if not os.path.exists(db_path):
            QMessageBox.warning(self, "エラー", f"データベースファイルが見つかりません:\n{db_path}")
            return

        block_no = self.cb_location.currentData()
        table_name = self.cb_table.currentText()
        start_str = self.date_start.date().toString("yyyy-MM-dd")
        end_str = self.date_end.date().toString("yyyy-MM-dd")
        date_col = "ObsDay" if table_name == "UpperAir" else "ObsDate"

        if is_analysis:
            if table_name == "AmedasDaily":
                query = f"""
                SELECT COUNT(*) as データ日数, MAX(TempMax) as 期間中最高気温, MIN(TempMin) as 期間中最低気温, MAX(RainTot) as 期間中最大日雨量, SUM(RainTot) as 期間中総雨量, MAX(WindMax) as 期間中最大風速, SUM(CASE WHEN TempMax >= 35.0 THEN 1 ELSE 0 END) as 猛暑日_日数, SUM(CASE WHEN TempMax >= 30.0 THEN 1 ELSE 0 END) as 真夏日_日数, SUM(CASE WHEN TempMax >= 25.0 THEN 1 ELSE 0 END) as 夏日_日数, SUM(CASE WHEN TempMin < 0.0 THEN 1 ELSE 0 END) as 冬日_日数, SUM(CASE WHEN TempMax < 0.0 THEN 1 ELSE 0 END) as 真冬日_日数
                FROM {table_name} WHERE PointCode = '{block_no}' AND {date_col} BETWEEN '{start_str}' AND '{end_str}'
                """
            elif table_name == "AmedasHourly":
                query = f"""
                SELECT COUNT(*) as データ件数, MAX(Temperature) as 期間中最高気温, MIN(Temperature) as 期間中最低気温, MAX(Rain) as 期間中最大1時間雨量, SUM(Rain) as 期間中総雨量, MAX(WindSpeed) as 期間中最大風速
                FROM {table_name} WHERE PointCode = '{block_no}' AND {date_col} BETWEEN '{start_str}' AND '{end_str}'
                """
            elif table_name == "Amedas10Min":
                query = f"""
                SELECT COUNT(*) as データ件数, MAX(Temperature) as 期間中最高気温, MIN(Temperature) as 期間中最低気温, MAX(Rain) as 期間中最大10分雨量, SUM(Rain) as 期間中総雨量, MAX(MaxWind) as 期間中最大瞬間風速
                FROM {table_name} WHERE PointCode = '{block_no}' AND {date_col} BETWEEN '{start_str}' AND '{end_str}'
                """
            elif table_name == "UpperAir":
                query = f"""
                SELECT COUNT(*) as データ件数, Pressure as 気圧面, MAX(Temperature) as 期間中最高気温, MIN(Temperature) as 期間中最低気温, MAX(WindSpeed) as 期間中最大風速
                FROM {table_name} WHERE PointCode = '{block_no}' AND {date_col} BETWEEN '{start_str}' AND '{end_str}' GROUP BY Pressure ORDER BY Pressure DESC
                """
        else:
            query = f"SELECT * FROM {table_name} WHERE PointCode = '{block_no}' AND {date_col} BETWEEN '{start_str}' AND '{end_str}' ORDER BY {date_col} DESC"

        try:
            conn = sqlite3.connect(db_path)
            df = pd.read_sql_query(query, conn)
            conn.close()

            if df.empty or df.iloc[0].isna().all():
                QMessageBox.warning(self, "結果", "該当するデータが見つかりませんでした。")
                self.table_widget.setRowCount(0)
                self.table_widget.setColumnCount(0)
                return

            self.table_widget.setRowCount(df.shape[0])
            self.table_widget.setColumnCount(df.shape[1])
            self.table_widget.setHorizontalHeaderLabels(df.columns)

            for row in range(df.shape[0]):
                for col in range(df.shape[1]):
                    val = df.iat[row, col]
                    if isinstance(val, float):
                        item = QTableWidgetItem(f"{val:.1f}")
                    else:
                        item = QTableWidgetItem(str(val) if pd.notnull(val) else "")
                    item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
                    self.table_widget.setItem(row, col, item)
            
            self.table_widget.resizeColumnsToContents()

        except Exception as e:
            QMessageBox.critical(self, "エラー", f"データベース読み込みエラー:\n{e}")

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