未分類

# ==========================================
# 【App A】 GPV解析・キャッシュエンジン
# VERSION: 81.0 (上空データ抽出完全自動判別化・MSM渦度内包対応)
# ==========================================
import sys, os, glob, re, subprocess
from datetime import datetime

os.environ['QT_API'] = 'pyqt6'
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, 
                             QPushButton, QLabel, QListWidget, QFileDialog, QMessageBox,
                             QSystemTrayIcon, QMenu)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QSettings
from PyQt6.QtGui import QIcon, QPixmap, QPainter, QColor, QAction

try: import cfgrib
except ImportError: sys.exit(1)

APP_DIR = os.getcwd()
DEFAULT_OUTPUT_DIR = os.path.join(APP_DIR, "gpv_cache_npz")
CURRENT_OUTPUT_DIR = DEFAULT_OUTPUT_DIR  
WGRIB2_EXE = os.path.join(APP_DIR, "wgrib2_data", "wgrib2.exe") 

os.makedirs(CURRENT_OUTPUT_DIR, exist_ok=True)

def write_syslog(msg):
    log_file = os.path.join(CURRENT_OUTPUT_DIR, "system_log.txt")
    try:
        with open(log_file, "a", encoding="utf-8") as f: 
            f.write(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}\n")
    except Exception: pass

WORKER_SCRIPT = os.path.join(APP_DIR, "_gpv_worker.py")

def create_lightning_icon():
    pixmap = QPixmap(64, 64)
    pixmap.fill(Qt.GlobalColor.transparent)
    painter = QPainter(pixmap)
    font = painter.font()
    font.setPixelSize(50)
    painter.setFont(font)
    painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "⚡")
    painter.end()
    return QIcon(pixmap)

WORKER_CODE = """
import sys, traceback

def main():
    try:
        import os, gc, json, subprocess
        import numpy as np
        import warnings; warnings.filterwarnings("ignore")
        import cfgrib
        os.environ["ECCODES_MAX_VALUES"] = "5000000"

        if sys.platform == "win32":
            conda_dir = os.path.dirname(sys.executable)
            dll_paths = [os.path.join(conda_dir, "Library", "bin"), os.path.join(conda_dir, "bin")]
            for p in dll_paths:
                if os.path.exists(p):
                    os.environ["PATH"] = f"{p};{os.environ.get('PATH', '')}"
                    try: os.add_dll_directory(p)
                    except Exception: pass

        def calculate_vorticity(u, v, lon, lat):
            try:
                R = 6371000.0; LON, LAT = np.meshgrid(lon, lat) if lon.ndim == 1 else (lon, lat)
                rad_lat = np.deg2rad(LAT); rad_lon = np.deg2rad(LON)
                dy = R * np.gradient(rad_lat, axis=0); dx = R * np.cos(rad_lat) * np.gradient(rad_lon, axis=1)
                dx[dx == 0] = 1e-10; dy[dy == 0] = 1e-10
                return ((np.gradient(v, axis=1) / dx) - (np.gradient(u, axis=0) / dy)) * 1e5
            except Exception: return np.zeros_like(u)

        model = sys.argv[1]; mode = sys.argv[2]; cache = sys.argv[3]
        init = sys.argv[4]; target_fts_str = sys.argv[5]
        f1 = sys.argv[6]; f2 = sys.argv[7]; wgrib2_path = sys.argv[8]
        
        target_fts = [int(x) for x in target_fts_str.split(',')]
        d_all = {ft: {} for ft in target_fts}
        creationflags = 0x08000000 if sys.platform == "win32" else 0

        for t_ft in target_fts:
            d = d_all[t_ft]
            
            def slice_with_wgrib2(fin, ft_val, suffix):
                if fin == "NONE": return "NONE"
                fout = os.path.join(cache, f"temp_{model}_{init}_{ft_val}_{suffix}.bin")
                if ft_val == 0: match_str = ":(anl|0 hour [^:]+|[0-9]+\\\\-0 hour [^:]+):"
                else: match_str = f":({ft_val} hour [^:]+|[0-9]+\\\\-{ft_val} hour [^:]+):"
                
                cmd = [wgrib2_path, fin, "-match", match_str, "-grib", fout]
                try:
                    subprocess.run(cmd, creationflags=creationflags, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    if os.path.exists(fout) and os.path.getsize(fout) > 0: return fout
                except Exception: pass
                return "NONE"

            f1_mini = slice_with_wgrib2(f1, t_ft, "1")
            f2_mini = slice_with_wgrib2(f2, t_ft, "2")

            def process_file(filepath):
                if filepath == "NONE": return
                try:
                    dss = cfgrib.open_datasets(filepath, backend_kwargs={'indexpath': ''})
                    for ds in dss:
                        lon = ds.longitude.values if hasattr(ds, 'longitude') else None
                        lat = ds.latitude.values if hasattr(ds, 'latitude') else None

                        for v in ds.data_vars:
                            da = ds[v]
                            da_step = da.isel(step=0) if 'step' in da.dims else da
                            if 'number' in da_step.coords: da_step = da_step.isel(number=0)
                            
                            sName = str(da.attrs.get('GRIB_shortName', v)).lower()
                            attrs_str = str(da.attrs).lower()
                            disc = da.attrs.get('GRIB_discipline', -1)
                            cat = da.attrs.get('GRIB_parameterCategory', -1)
                            num = da.attrs.get('GRIB_parameterNumber', -1)
                            
                            is_upper = 'isobaricInhPa' in da_step.coords or 'level' in da_step.coords
                            
                            if not is_upper and 'lon_surf' not in d and lon is not None:
                                d['lon_surf'] = lon; d['lat_surf'] = lat; d['lon'] = lon; d['lat'] = lat
                            elif is_upper and 'lon_pall' not in d and lon is not None:
                                d['lon_pall'] = lon; d['lat_pall'] = lat

                            if mode in ["GPV", "ANAL"]:
                                if not is_upper:
                                    val = da_step.values.copy()
                                    while val.ndim > 2: val = val[0]
                                    
                                    if sName in ['prmsl', 'msl', 'mslet']:
                                        d['slp'] = val / 100.0 if np.nanmax(val) > 2000 else val
                                    elif sName in ['pres', 'sp'] or 'pressure' in attrs_str:
                                        if 'slp' not in d: d['pres'] = val / 100.0 if np.nanmax(val) > 2000 else val
                                    elif sName in ['10u', 'u', 'u10'] or ('u-component' in attrs_str and '10' in attrs_str):
                                        d['u10'] = val
                                    elif sName in ['10v', 'v', 'v10'] or ('v-component' in attrs_str and '10' in attrs_str):
                                        d['v10'] = val
                                    elif sName in ['2t', 't', 't2m', 'temp'] or ('temperature' in attrs_str and '2' in attrs_str):
                                        d['t2m'] = val - 273.15 if np.nanmax(val) > 150 else val
                                    elif sName in ['2r', 'r', 'rh2m', 'rh'] or ('humidity' in attrs_str):
                                        d['rh2m'] = val
                                    elif sName in ['tcc', 'hcc', 'mcc', 'lcc']:
                                        d[sName] = val
                                    elif sName in ['tp', 'apcp', 'pr', 'precip'] or 'precip' in attrs_str or 'accum' in attrs_str:
                                        if 'precip' not in d: d['precip'] = np.nan_to_num(val, nan=0.0)
                                else:
                                    levels = []
                                    if 'isobaricInhPa' in da_step.coords: levels = np.atleast_1d(da_step.isobaricInhPa.values)
                                    elif 'level' in da_step.coords: levels = np.atleast_1d(da_step.level.values)
                                    
                                    for l_idx, lvl in enumerate(levels):
                                        lvl = int(lvl)
                                        if lvl in [300, 500, 600, 700, 850, 925, 950, 975]:
                                            if len(levels) > 1:
                                                dim_n = 'isobaricInhPa' if 'isobaricInhPa' in da_step.coords else 'level'
                                                val_l = da_step.isel(**{dim_n: l_idx}).values.copy()
                                            else:
                                                val_l = da_step.values.copy()
                                            
                                            while val_l.ndim > 2: val_l = val_l[0]
                                            
                                            if sName in ['t', 'temp'] or 'temperature' in attrs_str: d[f't{lvl}'] = val_l - 273.15 if np.nanmax(val_l) > 150 else val_l
                                            elif sName in ['u', 'u-component']: d[f'u{lvl}'] = val_l
                                            elif sName in ['v', 'v-component']: d[f'v{lvl}'] = val_l
                                            elif sName in ['r', 'rh', 'humidity']: d[f'r{lvl}'] = val_l
                                            elif sName in ['w', 'v-velocity', 'dz']: d[f'w{lvl}'] = val_l
                                            elif sName in ['gh', 'z', 'geopotential']: d[f'gh{lvl}'] = val_l
                            elif mode == "GUID":
                                val = da_step.values.copy()
                                while val.ndim > 2: val = val[0]
                                
                                # GRIB2の生の名前またはIDで確実に全データを保存
                                if sName != 'unknown': d[sName] = val
                                else: d[f"var_{disc}_{cat}_{num}"] = val
                                
                                # 代表的なキー名(エイリアス)へ紐付け
                                if sName in ['2t', 't', 't2m', 'tmp', 'temp'] or (disc == 0 and cat == 0 and num == 0) or 'temperature' in attrs_str: 
                                    d['t2m'] = val - 273.15 if np.nanmax(val) > 150 else val  # ケルビン→摂氏変換を復旧
                                elif sName in ['2r', 'r', 'rh2m', 'rh'] or (disc == 0 and cat == 1 and num == 1) or 'humidity' in attrs_str: 
                                    d['rh2m'] = val
                                elif sName in ['10u', 'u', 'u10', 'ugrd'] or (disc == 0 and cat == 2 and num == 2): 
                                    d['u10'] = val
                                elif sName in ['10v', 'v', 'v10', 'vgrd'] or (disc == 0 and cat == 2 and num == 3): 
                                    d['v10'] = val
                                elif sName in ['tp', 'apcp', 'pr', 'precip'] or (disc == 0 and cat == 1 and num in [8, 52]) or 'precip' in attrs_str or 'accum' in attrs_str: 
                                    if 'precip' not in d: d['precip'] = np.nan_to_num(val, nan=0.0)
                                elif sName in ['weasd', 'snod', 'snow', 'asnow'] or (disc == 0 and cat == 1 and num in [11, 13, 29, 60]) or 'snow' in attrs_str: 
                                    d['snow'] = val  # asnowを追加
                                elif sName in ['wea', 'nswrs', 'nswrv', 'weather'] or (disc == 0 and cat == 19 and num == 192) or 'weather' in attrs_str: 
                                    d['wea'] = val
                                elif sName in ['thund', 'lig', 'ltng', 'thunder', 'prstm', 'tstm'] or (disc == 0 and cat == 19 and num == 193) or 'thunder' in attrs_str: 
                                    d['thund'] = val # tstmを追加
                    for ds in dss: ds.close()
                except Exception: pass
                finally: gc.collect()

            process_file(f1_mini)
            process_file(f2_mini)
            
            try:
                if f1_mini != "NONE" and os.path.exists(f1_mini): os.remove(f1_mini)
                if f2_mini != "NONE" and os.path.exists(f2_mini): os.remove(f2_mini)
            except Exception: pass

        for t_ft in target_fts:
            d = d_all[t_ft]
            if not d: continue

            if mode in ["GPV", "ANAL"]:
                for lvl in [300, 500, 600, 700, 850, 925, 950, 975]:
                    tc = d.get(f't{lvl}'); rh = d.get(f'r{lvl}')
                    if tc is not None and rh is not None:
                        rh_c = np.clip(rh, 0.1, 100)
                        e = 6.112 * np.exp((17.67*tc)/(tc+243.5)) * (rh_c/100.0)
                        td = (243.5*np.log(e/6.112))/(17.67-np.log(e/6.112))
                        d[f'tddep{lvl}'] = tc - td
                        tk = tc + 273.15; theta = tk*(1000.0/lvl)**0.2854; w = 0.622*e/(lvl-e)
                        d[f'ep{lvl}'] = theta * np.exp((2.5e6*w)/(1004.0*tk))
                u500 = d.get('u500'); v500 = d.get('v500')
                if u500 is not None and v500 is not None:
                    vort_lon = d.get('lon_pall') if 'lon_pall' in d else d.get('lon_surf')
                    vort_lat = d.get('lat_pall') if 'lat_pall' in d else d.get('lat_surf')
                    if vort_lon is not None and vort_lat is not None:
                        d['vort500'] = calculate_vorticity(u500, v500, vort_lon, vort_lat)
            
            pfx = "GUID_" if mode == "GUID" else ""
            np.savez_compressed(os.path.join(cache, f"{model}_{pfx}{init}_FT{t_ft:02d}.npz"), **d)
            print(f"SUCCESS:{t_ft}", flush=True)

    except BaseException as e:
        print(f"CRITICAL_ERROR: {traceback.format_exc()}", flush=True)
        sys.exit(1)

if __name__ == '__main__': main()
"""

try:
    with open(WORKER_SCRIPT, "w", encoding="utf-8") as f: f.write(WORKER_CODE)
except Exception: pass

class DataParserThread(QThread):
    log_signal = pyqtSignal(str); progress_signal = pyqtSignal(int, str); finished_signal = pyqtSignal(bool)
    def __init__(self, folder_paths, output_dir, last_reported):
        super().__init__()
        self.folder_paths = [path for path in folder_paths if path and os.path.exists(path)]
        self.output_dir = output_dir
        self.last_reported = last_reported; self.abort = False
        
    def run(self):
        if not self.folder_paths: self.finished_signal.emit(False); return
        if not os.path.exists(WGRIB2_EXE):
            self.log_signal.emit(f"⚠️ wgrib2が見つかりません。"); self.finished_signal.emit(False); return

        has_new_data = False; scan_started = False; reported_new_file = False
        creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
        conda_dir = os.path.dirname(sys.executable); lib_bin = os.path.join(conda_dir, "Library", "bin")
        env = os.environ.copy(); env["PATH"] = f"{lib_bin};{env.get('PATH', '')}"; env["PYTHONIOENCODING"] = "utf-8"
        
        for folder_path in self.folder_paths:
            if self.abort: break
            msm_files_all = glob.glob(os.path.join(folder_path, '**', '*MSM*GPV*.bin'), recursive=True)
            gsm_files_raw = glob.glob(os.path.join(folder_path, '**', '*GSM*GPV*.bin'), recursive=True)
            gsm_jp_files = [f for f in gsm_files_raw if 'Rjp' in f or 'Japan' in f or 'jp' in f.lower()]
            gsm_files_all = gsm_jp_files if gsm_jp_files else gsm_files_raw
            
            gsm_guid = glob.glob(os.path.join(folder_path, '**', '*GSM_GUID*Toorg*.bin'), recursive=True)
            msm_guid = glob.glob(os.path.join(folder_path, '**', '*MSM_GUID*Toorg*.bin'), recursive=True)
            meps_gpv = glob.glob(os.path.join(folder_path, '**', '*MEPS_GPV*.bin'), recursive=True)
            anal_files_all = glob.glob(os.path.join(folder_path, '**', '*ANAL_grib2*.bin'), recursive=True) 
            
            def get_latest_files(file_list):
                if not file_list: return [], "UNKNOWN"
                times = set(re.search(r'_(\d{14})_', os.path.basename(f)).group(1) for f in file_list if re.search(r'_(\d{14})_', f))
                if not times: return file_list, "UNKNOWN"
                latest_time = max(times)
                return [f for f in file_list if latest_time in f], latest_time

            msm_files, msm_init = get_latest_files(msm_files_all)
            gsm_files, gsm_init = get_latest_files(gsm_files_all)
            gg_files, gg_init = get_latest_files(gsm_guid)
            mg_files, mg_init = get_latest_files(msm_guid)
            meps_files, meps_init = get_latest_files(meps_gpv)
            anal_files, anal_init = get_latest_files(anal_files_all)

            phases = [
                ('GSM_JP', gsm_files, gsm_init, list(range(0, 265, 3)), "GPV"),
                ('MSM', msm_files, msm_init, list(range(0, 79, 1)), "GPV"),
                ('GSM', gg_files, gg_init, list(range(0, 265, 3)), "GUID"), 
                ('MSM', mg_files, mg_init, list(range(0, 79, 1)), "GUID"),
                ('MEPS', meps_files, meps_init, list(range(0, 40, 3)), "GPV"),
                ('ANAL', anal_files, anal_init, [0], "ANAL") 
            ]
            
            for model_name, files, init_time_str, ft_list, mode in phases:
                if self.abort or not files or init_time_str == "UNKNOWN": continue
                
                if self.last_reported.get(f"{model_name}_{mode}") != init_time_str:
                    self.log_signal.emit(f"✅ {model_name} {mode} 最新データ発見: {init_time_str}")
                    self.last_reported[f"{model_name}_{mode}"] = init_time_str; reported_new_file = True

                tasks = []
                for ft in ft_list:
                    pfx = "GUID_" if mode == "GUID" else ""
                    npz_file = os.path.join(self.output_dir, f"{model_name}_{pfx}{init_time_str}_FT{ft:02d}.npz")
                    if not os.path.exists(npz_file): tasks.append(ft)

                if not tasks: continue

                if not scan_started:
                    self.log_signal.emit("🔍 新規データを抽出中..."); scan_started = True; has_new_data = True

                proc_name = "GPV" if mode in ["GPV", "ANAL"] else "ガイダンス"
                file_groups = {}
                for ft in tasks:
                    if mode == "GPV":
                        f1 = self._get_file_for_ft(files, "Lsurf", ft) or "NONE"
                        f2 = self._get_file_for_ft(files, "P-all" if any("P-all" in x for x in files) else "L-pall", ft) or "NONE"
                    elif mode == "ANAL": 
                        f1 = files[0]; f2 = "NONE"
                    else:
                        f1 = files[0]; f2 = "NONE"
                    
                    if f1 == "NONE" and f2 == "NONE": continue
                    key = (f1, f2)
                    if key not in file_groups: file_groups[key] = []
                    file_groups[key].append(ft)

                self.log_signal.emit(f"🚀 {model_name} {proc_name} 爆速ハイブリッド解析 (対象: {len(tasks)}件)...")

                total_extracted = 0
                for (f1, f2), fts in file_groups.items():
                    if self.abort: break
                    for i in range(0, len(fts), 5):
                        if self.abort: break
                        chunk_fts = fts[i:i + 5]; ft_str = ",".join(map(str, chunk_fts))
                        cmd = [sys.executable, WORKER_SCRIPT, model_name, mode, self.output_dir, init_time_str, ft_str, f1, f2, WGRIB2_EXE]
                        process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creationflags, env=env)
                        for line in iter(process.stdout.readline, ''):
                            line = line.strip()
                            if not line: continue
                            if line.startswith("SUCCESS:"):
                                ft_success = line.split(":")[1]; total_extracted += 1
                                self.progress_signal.emit(int((total_extracted / len(tasks)) * 100), f"⚡ wgrib2解析中 {model_name} FT={ft_success}h...")
                                self.log_signal.emit(f"⚡ 爆速抽出完了: {model_name} {proc_name} FT={ft_success}h")
                            elif "CRITICAL_ERROR:" in line or "Traceback" in line or "Error:" in line or "File " in line:
                                self.log_signal.emit(f"⚠️ {line}")
                        process.stdout.close(); process.wait()

        if has_new_data or reported_new_file: self.log_signal.emit("✅ スキャン・抽出完了")
        self.finished_signal.emit(has_new_data)

    def _get_file_for_ft(self, files_list, keyword, ft):
        is_gsm = any("GSM_GPV" in f for f in files_list)
        if is_gsm:
            for f in files_list:
                if keyword not in f: continue
                m = re.search(r'FD(\d{2})(\d{2})-(\d{2})(\d{2})', os.path.basename(f))
                if m and int(m.group(1))*24 + int(m.group(2)) <= ft <= int(m.group(3))*24 + int(m.group(4)): return f
            return next((f for f in files_list if keyword in f), None)
        for f in files_list:
            if keyword in f:
                m = re.search(r'FH(\d{2})-(\d{2})', os.path.basename(f))
                if m and int(m.group(1)) <= ft <= int(m.group(2)): return f
                elif not m: return f
        return None

class EngineStatusWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("GPV 解析エンジン (Ver 81.0)")
        self.setFixedSize(650, 580)
        self.setStyleSheet("""
            QWidget { background-color: #0A192F; color: #E0E0E0; font-family: 'MS Gothic'; font-size: 11pt; }
            QLabel { color: #64FFDA; font-weight: bold; }
            QPushButton { background-color: #1D3557; color: white; padding: 6px; border-radius: 4px; font-weight: bold; border: 1px solid #457B9D; }
            QPushButton:hover { background-color: #457B9D; }
            QListWidget { background-color: #112240; border: 1px solid #457B9D; color: #64FFDA; padding: 5px; font-family: Consolas, monospace; font-size: 10pt; }
        """)
        
        layout = QVBoxLayout(self)
        self.status_label = QLabel("状態: 起動中..."); self.status_label.setStyleSheet("font-size: 14pt;")
        layout.addWidget(self.status_label)
        
        control_layout = QHBoxLayout()
        self.pause_btn = QPushButton("⏸ 一時停止"); self.resume_btn = QPushButton("▶ 手動強制スキャン"); self.stop_btn = QPushButton("⏹ 停止")
        self.clear_btn = QPushButton("🧹 キャッシュ全削除")
        for btn in [self.pause_btn, self.resume_btn, self.stop_btn]: control_layout.addWidget(btn)
        self.clear_btn.setStyleSheet("background-color: #C0392B;"); control_layout.addWidget(self.clear_btn)
        layout.addLayout(control_layout)

        out_layout = QHBoxLayout()
        self.out_folder_btn = QPushButton("💾 出力先フォルダ(共有先)を設定"); self.out_folder_btn.setStyleSheet("background-color: #2980B9;")
        out_layout.addWidget(self.out_folder_btn)
        self.out_folder_label = QLabel("未設定"); self.out_folder_label.setStyleSheet("background: #112240; padding: 4px; border: 1px solid #457B9D; color: #8892B0;")
        out_layout.addWidget(self.out_folder_label, stretch=1)
        layout.addLayout(out_layout)
        
        self.log_list = QListWidget(); layout.addWidget(self.log_list)
        self.copy_btn = QPushButton("📋 ログをコピー"); self.copy_btn.setStyleSheet("background-color: #16A085;"); layout.addWidget(self.copy_btn)
        
        folder_layout = QHBoxLayout()
        self.folder_btns = [QPushButton(f"📁 読込元フォルダ {i+1}") for i in range(3)]
        for b in self.folder_btns: folder_layout.addWidget(b)
        layout.addLayout(folder_layout)
        
        self.exit_btn = QPushButton("システムを完全に終了"); self.exit_btn.setStyleSheet("background-color: #8b0000;"); layout.addWidget(self.exit_btn)

    def log(self, message):
        self.log_list.addItem(f"[{datetime.now().strftime('%H:%M:%S')}] {message}")
        self.log_list.scrollToBottom(); write_syslog(message)

    def closeEvent(self, event):
        event.ignore(); self.hide()
        if hasattr(self, 'tray_msg_callback'): self.tray_msg_callback()

class TrayApp(QApplication):
    def __init__(self, sys_argv):
        super().__init__(sys_argv)
        self.setQuitOnLastWindowClosed(False) 
        self.settings = QSettings("SapporoWeatherApp", "GPVEngine")
        self.monitor_folders = [self.settings.value(f"watch_dir_{i+1}", "") for i in range(3)]
        
        global CURRENT_OUTPUT_DIR
        self.output_dir = self.settings.value("output_dir", DEFAULT_OUTPUT_DIR)
        CURRENT_OUTPUT_DIR = self.output_dir
        os.makedirs(self.output_dir, exist_ok=True)

        self.last_reported = {}
        self.window = EngineStatusWindow()
        self.window.tray_msg_callback = self.show_tray_message
        
        self.tray_icon = QSystemTrayIcon(self)
        self.tray_icon.setIcon(create_lightning_icon())
        self.tray_icon.setToolTip("GPV抽出エンジン (稼働中)")
        
        tray_menu = QMenu(); show_action = QAction("ウィンドウを表示"); show_action.triggered.connect(self.restore_window)
        quit_action = QAction("完全に終了する"); quit_action.triggered.connect(self.quit)
        tray_menu.addAction(show_action); tray_menu.addAction(quit_action)
        self.tray_icon.setContextMenu(tray_menu); self.tray_icon.activated.connect(self.tray_icon_activated); self.tray_icon.show()
        
        with open(os.path.join(self.output_dir, "system_log.txt"), "w", encoding="utf-8") as f: pass
        self.window.log("===============================")
        self.window.log("システム起動: Ver 81.0 (上空完全自動判別化)")
        self.window.log(f"wgrib2 パス確認: {'✅ 認識完了' if os.path.exists(WGRIB2_EXE) else '⚠️ 未発見'}")
        self.window.log("===============================")
        
        self.window.out_folder_label.setText(self.output_dir)
        self.window.out_folder_btn.clicked.connect(self.select_output_folder)

        folders_set = False
        for i in range(3):
            self.window.folder_btns[i].clicked.connect(lambda checked, idx=i: self.select_folder(idx))
            self.update_folder_btn_text(i)
            if self.monitor_folders[i]: folders_set = True
                
        if not folders_set: self.window.status_label.setText("状態: 読込元フォルダ未設定")

        self.window.copy_btn.clicked.connect(self.copy_logs); self.window.pause_btn.clicked.connect(self.pause_parsing)
        self.window.resume_btn.clicked.connect(self.force_scan); self.window.stop_btn.clicked.connect(self.stop_parsing)
        self.window.clear_btn.clicked.connect(self.clear_cache); self.window.exit_btn.clicked.connect(self.quit)
        
        self.timer = QTimer(); self.timer.timeout.connect(self.run_parser); self.timer.start(10000)
        self.is_processing = False; self.run_parser(); self.window.show()

    def tray_icon_activated(self, reason):
        if reason == QSystemTrayIcon.ActivationReason.DoubleClick: self.restore_window()

    def restore_window(self):
        self.window.showNormal(); self.window.activateWindow()

    def show_tray_message(self):
        self.tray_icon.showMessage("エンジン稼働中", "ウィンドウを閉じましたが、裏で抽出を続けています。終了する場合は右クリックから「完全に終了する」を選んでください。", QIcon(self.tray_icon.icon()), 3000)

    def select_output_folder(self):
        global CURRENT_OUTPUT_DIR
        folder = QFileDialog.getExistingDirectory(self.window, "出力先フォルダ(共有)を選択", self.output_dir)
        if folder:
            self.output_dir = folder; CURRENT_OUTPUT_DIR = folder; self.settings.setValue("output_dir", folder)
            self.window.out_folder_label.setText(folder); self.window.log(f"💾 出力先フォルダを更新しました: {folder}"); self.run_parser()

    def clear_cache(self):
        reply = QMessageBox.question(self.window, "確認", "現在の出力先のデータをすべて削除し再抽出しますか?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if reply == QMessageBox.StandardButton.Yes:
            self.timer.stop(); self.window.log("🗑️ キャッシュデータのお掃除を開始します...")
            for f in glob.glob(os.path.join(self.output_dir, "*.npz")) + glob.glob(os.path.join(self.output_dir, "*.json")) + glob.glob(os.path.join(self.output_dir, "*.bin")):
                try: os.remove(f)
                except Exception: pass
            for d in self.monitor_folders:
                if d:
                    for idx_f in glob.glob(os.path.join(d, '**', '*.idx'), recursive=True):
                        try: os.remove(idx_f)
                        except Exception: pass
            self.last_reported = {}; self.window.log(f"✨ データを削除しました。再抽出を開始します!")
            self.run_parser(); self.timer.start(10000)

    def select_folder(self, index):
        folder = QFileDialog.getExistingDirectory(self.window, "読込元フォルダ選択")
        if folder:
            self.monitor_folders[index] = folder; self.settings.setValue(f"watch_dir_{index+1}", folder)
            self.update_folder_btn_text(index); self.window.log(f"フォルダ{index+1}を更新: {folder}"); self.run_parser()

    def update_folder_btn_text(self, i):
        f = self.monitor_folders[i]; self.window.folder_btns[i].setText(f"📁 {os.path.basename(f)}" if f else f"📁 未設定 {i+1}")
    
    def copy_logs(self): QApplication.clipboard().setText("\n".join([self.window.log_list.item(i).text() for i in range(self.window.log_list.count())]))
    def pause_parsing(self): self.timer.stop(); self.window.log("一時停止しました")
    def force_scan(self): self.window.log("手動強制スキャンを開始します..."); self.timer.start(10000); self.run_parser()
    def stop_parsing(self): self.timer.stop(); self.window.log("停止しました")

    def run_parser(self):
        if self.is_processing: return
        if not any(self.monitor_folders): self.window.status_label.setText("状態: 読込元フォルダ未設定"); return
        self.is_processing = True
        self.worker = DataParserThread(self.monitor_folders, self.output_dir, self.last_reported)
        self.worker.log_signal.connect(self.window.log); self.worker.progress_signal.connect(lambda v, t: self.window.status_label.setText(f"状態: {t}"))
        self.worker.finished_signal.connect(self.on_parse_finished)
        self.worker.start()

    def on_parse_finished(self, has_new_data): 
        self.is_processing = False; self.window.status_label.setText("状態: 待機中 (自動スキャン継続中)")

if __name__ == '__main__':
    app = TrayApp(sys.argv); sys.exit(app.exec())

コメント