import sys
import os
import sqlite3
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, timezone
import re
import subprocess
import glob
import logging
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget,
QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QFileDialog, QComboBox, QDateEdit, QMessageBox,
QGroupBox, QSystemTrayIcon, QStyle, QCheckBox,
QFrame, QGraphicsView, QGraphicsScene, QMenu,
QGridLayout)
from PyQt6.QtCore import QDate, Qt, QTimer, QSettings, QThread, pyqtSignal
from PyQt6.QtGui import QAction, QPixmap, QImageReader
def resource_path(relative_path):
try:
base_path = sys._MEIPASS
except Exception:
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)
# ==========================================
# バックグラウンド処理専用スレッド (非同期・ANAL専用)
# ==========================================
class ExtractionWorker(QThread):
progress_signal = pyqtSignal(str)
finished_signal = pyqtSignal(int)
error_signal = pyqtSignal(str)
def __init__(self, target_files, col_name, db_path, wgrib2_path, lat_min, lat_max, lon_min, lon_max):
super().__init__()
self.target_files = target_files
self.col_name = col_name
self.db_path = db_path
self.wgrib2_path = wgrib2_path
self.lat_min = lat_min
self.lat_max = lat_max
self.lon_min = lon_min
self.lon_max = lon_max
def get_date_from_filename(self, f):
m = re.search(r'(20[2-9]\d{5,11})', os.path.basename(f))
if m:
ds = m.group(1)
if len(ds) >= 12: return ds[:12]
elif len(ds) == 10: return ds + "00"
elif len(ds) == 8: return ds + "0000"
return "000000000000"
def run(self):
total_inserted = 0
try:
existing_times = set()
with sqlite3.connect(self.db_path, timeout=30.0) as conn:
conn.execute("PRAGMA journal_mode=WAL;")
cursor = conn.cursor()
cursor.execute("UPDATE GPV_Points SET PointIDText = substr('00000' || PointID, -5, 5) WHERE PointIDText IS NULL")
db_points = pd.read_sql_query("SELECT PointIDText, Lon, Lat FROM GPV_Points", conn)
db_points['Lon'] = db_points['Lon'].round(4)
db_points['Lat'] = db_points['Lat'].round(4)
total_pts = len(db_points)
if total_pts > 0:
cursor.execute(f"SELECT ObsDay, ObsTime, COUNT({self.col_name}) FROM OBS_GP WHERE {self.col_name} IS NOT NULL GROUP BY ObsDay, ObsTime")
for row in cursor.fetchall():
if row[2] >= total_pts:
existing_times.add((row[0], row[1]))
sql_upsert = f"INSERT INTO OBS_GP (ObsDay, ObsTime, MsCd, {self.col_name}) VALUES (?, ?, ?, ?) ON CONFLICT(ObsDay, ObsTime, MsCd) DO UPDATE SET {self.col_name} = excluded.{self.col_name};"
for i, target_file in enumerate(self.target_files):
dt_str = self.get_date_from_filename(target_file)
if dt_str == "000000000000": continue
dt_utc = datetime.strptime(dt_str, "%Y%m%d%H%M")
obs_day = dt_utc.strftime("%Y-%m-%d")
obs_time = dt_utc.strftime("%H:%M")
if (obs_day, obs_time) in existing_times:
self.progress_signal.emit(f"スキップ済 ({i+1}/{len(self.target_files)}): {obs_day} {obs_time}UTC")
continue
self.progress_signal.emit(f"処理中 ({i+1}/{len(self.target_files)}): {obs_day} {obs_time}UTC")
# ★★★ 修正ポイント:コマンドプロンプト画面を非表示にする設定 ★★★
cflags = 0
if sys.platform == "win32":
cflags = subprocess.CREATE_NO_WINDOW # Windows環境でのみ画面作成をブロックする
proc = subprocess.Popen([self.wgrib2_path, target_file, "-no_header", "-csv", "-"],
stdout=subprocess.PIPE,
text=True,
creationflags=cflags) # フラグを追加
f_df_list = []
for chunk in pd.read_csv(proc.stdout, header=None, engine='c', chunksize=100000):
val_1 = pd.to_numeric(chunk.iloc[:, -3], errors='coerce')
val_2 = pd.to_numeric(chunk.iloc[:, -2], errors='coerce')
chunk["Value"] = pd.to_numeric(chunk.iloc[:, -1], errors='coerce')
if val_1.mean() > 100:
chunk["Lon"], chunk["Lat"] = val_1, val_2
else:
chunk["Lon"], chunk["Lat"] = val_2, val_1
chunk["Lon"] = chunk["Lon"].round(4)
chunk["Lat"] = chunk["Lat"].round(4)
chunk = chunk.dropna(subset=["Lon", "Lat", "Value"])
filtered = chunk[(chunk["Lat"] >= self.lat_min) & (chunk["Lat"] <= self.lat_max) &
(chunk["Lon"] >= self.lon_min) & (chunk["Lon"] <= self.lon_max)]
if not filtered.empty:
f_df_list.append(filtered)
proc.wait()
if not f_df_list: continue
f_df = pd.concat(f_df_list, ignore_index=True)
unique_coords = f_df[['Lon', 'Lat']].drop_duplicates()
merged_check = pd.merge(unique_coords, db_points, on=['Lon', 'Lat'], how='left')
missing_coords = merged_check[merged_check['PointIDText'].isna()]
if not missing_coords.empty:
new_coords = list(missing_coords[['Lon', 'Lat']].itertuples(index=False, name=None))
with sqlite3.connect(self.db_path, timeout=30.0) as conn:
cursor = conn.cursor()
cursor.executemany("INSERT OR IGNORE INTO GPV_Points (Lon, Lat) VALUES (?, ?)", new_coords)
cursor.execute("UPDATE GPV_Points SET PointIDText = substr('00000' || PointID, -5, 5) WHERE PointIDText IS NULL")
db_points = pd.read_sql_query("SELECT PointIDText, Lon, Lat FROM GPV_Points", conn)
db_points['Lon'] = db_points['Lon'].round(4)
db_points['Lat'] = db_points['Lat'].round(4)
f_df = pd.merge(f_df, db_points, on=['Lon', 'Lat'], how='left')
f_df = f_df.dropna(subset=['PointIDText', 'Value'])
f_df['ObsDay'] = obs_day
f_df['ObsTime'] = obs_time
f_df['MsCd'] = 'GPV_' + f_df['PointIDText'].astype(str)
bulk_data = list(f_df[['ObsDay', 'ObsTime', 'MsCd', 'Value']].itertuples(index=False, name=None))
if bulk_data:
with sqlite3.connect(self.db_path, timeout=60.0) as conn:
conn.execute("PRAGMA journal_mode=WAL;")
cursor = conn.cursor()
cursor.executemany(sql_upsert, bulk_data)
conn.commit()
total_inserted += len(bulk_data)
self.finished_signal.emit(total_inserted)
except Exception as e:
self.error_signal.emit(str(e))
class ZoomableView(QGraphicsView):
def __init__(self, scene, parent=None):
super().__init__(scene, parent)
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
def wheelEvent(self, event):
zoom_in_factor = 1.15
zoom_out_factor = 1.0 / zoom_in_factor
if event.angleDelta().y() > 0: zoom_factor = zoom_in_factor
else: zoom_factor = zoom_out_factor
self.scale(zoom_factor, zoom_factor)
class GPVExtractorApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("GPV解析値 (GRIB2) 高速抽出エンジン (Ver 26.1 - コマンド画面非表示版)")
self.resize(1150, 800)
self.really_quit = False
self.settings = QSettings("WeatherApp", "GPVExtractor_v26")
self.setStyleSheet("""
QMainWindow, QWidget { background-color: #F4F6F9; color: #333333; font-family: 'Segoe UI', 'Meiryo', sans-serif; }
QLabel, QCheckBox { color: #333333; font-weight: bold; }
QGroupBox { font-weight: bold; color: #0366D6; border: 1px solid #D0D7DE; border-radius: 6px; margin-top: 15px; background-color: #FFFFFF; }
QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; color: #0366D6; }
QPushButton { background-color: #0366D6; color: #FFFFFF; border: none; border-radius: 5px; padding: 8px 16px; font-weight: bold; }
QPushButton:hover { background-color: #0056B3; }
QPushButton:pressed { background-color: #004085; }
QPushButton:disabled { background-color: #D0D7DE; color: #8C959F; }
QPushButton#AutoBtn { background-color: #D73A49; color: #FFFFFF; }
QPushButton#AutoBtn:hover { background-color: #CB2431; }
QPushButton#ActionBtn { background-color: #28A745; color: #FFFFFF; }
QPushButton#ActionBtn:hover { background-color: #218838; }
QLineEdit, QComboBox, QDateEdit { border: 1px solid #D0D7DE; border-radius: 4px; padding: 5px; background-color: #FFFFFF; color: #000000; min-height: 25px; }
QComboBox QAbstractItemView { background-color: #FFFFFF; color: #000000; }
""")
default_db = os.path.abspath("weather_verification(削除禁止).db")
self.db_path = self.settings.value("last_db_path", default_db)
self.wgrib2_path = self.settings.value("wgrib2_path", resource_path("wgrib2.exe"))
self.map_img_path = self.settings.value("map_img_path", resource_path("gpv_reference_map.png"))
self.lat_min, self.lat_max = 42.0 + (40.0 / 60.0), 43.0 + (20.0 / 60.0)
self.lon_min, self.lon_max = 141.0 + (0.0 / 60.0), 141.0 + (40.0 / 60.0)
self.setup_tray_icon()
self.init_db()
self.init_ui()
self.t8_auto_timer = QTimer(self)
self.t8_auto_timer.timeout.connect(lambda: self.run_extraction_process("latest", silent=True))
QTimer.singleShot(100, self.reload_reference_map)
self.workers = []
def setup_tray_icon(self):
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_DriveNetIcon))
tray_menu = QMenu()
show_action = QAction("画面を表示する", self)
show_action.triggered.connect(self.showNormal)
quit_action = QAction("完全に終了する", self)
quit_action.triggered.connect(self.force_quit)
tray_menu.addAction(show_action)
tray_menu.addSeparator()
tray_menu.addAction(quit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.show()
self.tray_icon.activated.connect(self.on_tray_icon_activated)
def on_tray_icon_activated(self, reason):
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
self.showNormal()
self.activateWindow()
def closeEvent(self, event):
if not self.really_quit:
event.ignore()
self.hide()
self.tray_icon.showMessage("バックグラウンドで実行中", "GPV自動抽出処理は継続しています。", QSystemTrayIcon.MessageIcon.Information, 3000)
else:
event.accept()
def force_quit(self):
self.really_quit = True
QApplication.quit()
def init_db(self):
with sqlite3.connect(self.db_path, timeout=30.0) as conn:
conn.execute("PRAGMA journal_mode=WAL;")
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS OBS_GP (ObsDay TEXT, ObsTime TEXT, MsCd TEXT, Rainfall1h REAL, Snowfall1h REAL, SnowDepth REAL, PRIMARY KEY (ObsDay, ObsTime, MsCd))''')
cursor.execute('''CREATE TABLE IF NOT EXISTS GPV_Points (PointID INTEGER PRIMARY KEY AUTOINCREMENT, Lon REAL, Lat REAL, UNIQUE(Lon, Lat))''')
try:
cursor.execute("ALTER TABLE GPV_Points ADD COLUMN PointIDText TEXT")
except sqlite3.OperationalError:
pass
def init_ui(self):
main_widget = QWidget()
main_layout = QVBoxLayout(main_widget)
header = QHBoxLayout()
abs_db_path = os.path.abspath(self.db_path)
self.lbl_db_path = QLabel(f"🔗 統合共有DB: {abs_db_path} \n(※非同期WALモード稼働中)")
self.lbl_db_path.setStyleSheet("font-weight: bold; color: #0366D6; font-size: 14px; background-color: transparent;")
header.addWidget(self.lbl_db_path)
header.addStretch()
btn_quit = QPushButton("❌ 完全に終了する")
btn_quit.setStyleSheet("background-color: #CB2431; color: white; padding: 5px 15px; font-weight: bold;")
btn_quit.clicked.connect(self.force_quit)
header.addWidget(btn_quit)
main_layout.addLayout(header)
content_layout = QHBoxLayout()
content_layout.setSpacing(15)
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
left_panel.setFixedWidth(520)
group_settings = QGroupBox("0. システムパス設定")
settings_layout = QGridLayout()
settings_layout.addWidget(QLabel("DB パス:"), 0, 0)
btn_select_db = QPushButton("⚙️ DB変更")
btn_select_db.clicked.connect(self.select_db)
settings_layout.addWidget(btn_select_db, 0, 1)
settings_layout.addWidget(QLabel("wgrib2.exe:"), 1, 0)
self.lbl_wgrib2_path = QLabel(self.wgrib2_path)
self.lbl_wgrib2_path.setStyleSheet("font-size: 11px; color: #555; font-weight: normal;")
btn_select_wgrib2 = QPushButton("変更")
btn_select_wgrib2.clicked.connect(self.select_wgrib2)
settings_layout.addWidget(self.lbl_wgrib2_path, 1, 1)
settings_layout.addWidget(btn_select_wgrib2, 1, 2)
group_settings.setLayout(settings_layout)
left_layout.addWidget(group_settings)
group_folders = QGroupBox("1. 抽出対象の要素 & GRIB2フォルダ設定")
f_layout = QVBoxLayout()
self.chk_elements = {}
self.grib_folders = {}
self.grib_labels = {}
elements_info = [
("rain", "🌧️ 解析雨量", "Rainfall1h"),
("snowfall", "❄️ 解析降雪量", "Snowfall1h"),
("snowdepth", "⛄ 解析積雪深", "SnowDepth")
]
for key, name, db_col in elements_info:
row = QHBoxLayout()
chk = QCheckBox(name)
is_checked = self.settings.value(f"chk_{key}", True, type=bool)
chk.setChecked(is_checked)
chk.toggled.connect(lambda checked, k=key: self.settings.setValue(f"chk_{k}", checked))
self.chk_elements[key] = (chk, db_col)
current_folder = self.settings.value(f"folder_{key}", "")
self.grib_folders[key] = current_folder
lbl = QLabel(f"📁 {current_folder}" if current_folder else "📁 未設定")
lbl.setStyleSheet("font-size: 11px; font-weight: normal; color: #555;")
self.grib_labels[key] = lbl
btn_set = QPushButton("フォルダ選択")
btn_set.setStyleSheet("padding: 3px 8px; font-size: 11px;")
btn_set.clicked.connect(lambda _, k=key: self.select_grib_folder(k))
row.addWidget(chk)
row.addWidget(lbl, stretch=1)
row.addWidget(btn_set)
f_layout.addLayout(row)
group_folders.setLayout(f_layout)
left_layout.addWidget(group_folders)
group_actions = QGroupBox("2. 抽出実行 (※上部でチェックした要素を処理します)")
a_layout = QVBoxLayout()
auto_layout = QHBoxLayout()
self.btn_t8_auto = QPushButton("▶ 最新1件を取得 & 自動監視(10分毎)を開始")
self.btn_t8_auto.setObjectName("AutoBtn")
self.btn_t8_auto.clicked.connect(self.toggle_t8_auto)
self.lbl_t8_auto = QLabel("停止中")
self.lbl_t8_auto.setStyleSheet("color: #D73A49; font-weight: bold; background-color: transparent;")
auto_layout.addWidget(self.btn_t8_auto)
auto_layout.addWidget(self.lbl_t8_auto)
a_layout.addLayout(auto_layout)
self.btn_recent24 = QPushButton("🕒 直近24時間分を一括抽出 (高速)")
self.btn_recent24.setStyleSheet("background-color: #0366D6; color: white;")
self.btn_recent24.clicked.connect(lambda: self.run_extraction_process("recent24h"))
a_layout.addWidget(self.btn_recent24)
date_layout = QHBoxLayout()
self.grib_date_start = QDateEdit(QDate.currentDate().addDays(-1))
self.grib_date_start.setCalendarPopup(True)
self.grib_date_end = QDateEdit(QDate.currentDate())
self.grib_date_end.setCalendarPopup(True)
date_layout.addWidget(QLabel("開始(JST):"))
date_layout.addWidget(self.grib_date_start)
date_layout.addWidget(QLabel("〜 終了:"))
date_layout.addWidget(self.grib_date_end)
self.btn_range = QPushButton("📅 期間指定で一括抽出")
self.btn_range.setObjectName("ActionBtn")
self.btn_range.clicked.connect(lambda: self.run_extraction_process("range"))
date_layout.addWidget(self.btn_range)
a_layout.addLayout(date_layout)
self.lbl_status = QLabel("ステータス: 待機中")
self.lbl_status.setStyleSheet("color: #0366D6; font-weight: bold; margin-top: 5px;")
a_layout.addWidget(self.lbl_status)
group_actions.setLayout(a_layout)
left_layout.addWidget(group_actions)
group_query = QGroupBox("3. 解析値 簡易データ確認 (※日本時間JST指定)")
query_layout = QVBoxLayout()
dt_layout = QHBoxLayout()
self.t8_q_date = QDateEdit(QDate.currentDate())
self.t8_q_date.setCalendarPopup(True)
self.t8_q_time = QComboBox()
self.t8_q_time.addItem("日合計 / 日最大 (24h)")
self.t8_q_time.addItems([f"{str(h).zfill(2)}:00" for h in range(24)])
dt_layout.addWidget(QLabel("日時(JST):"))
dt_layout.addWidget(self.t8_q_date)
dt_layout.addWidget(self.t8_q_time)
query_layout.addLayout(dt_layout)
pt_layout = QHBoxLayout()
self.t8_q_point = QComboBox()
self.update_t8_query_points()
pt_layout.addWidget(QLabel("地点:"))
pt_layout.addWidget(self.t8_q_point)
query_layout.addLayout(pt_layout)
el_layout = QHBoxLayout()
self.t8_q_element = QComboBox()
self.t8_q_element.addItems(["降水量 (Rainfall1h)", "降雪量 (Snowfall1h)", "積雪深 (SnowDepth)"])
el_layout.addWidget(QLabel("要素:"))
el_layout.addWidget(self.t8_q_element)
query_layout.addLayout(el_layout)
res_layout = QHBoxLayout()
btn_query = QPushButton("値を確認")
btn_query.setObjectName("ActionBtn")
btn_query.clicked.connect(self.run_t8_simple_query)
self.lbl_t8_q_result = QLabel("結果: --")
self.lbl_t8_q_result.setStyleSheet("font-size: 15px; font-weight: bold; color: #D73A49; background-color: transparent;")
res_layout.addWidget(btn_query)
res_layout.addWidget(self.lbl_t8_q_result)
res_layout.addStretch()
query_layout.addLayout(res_layout)
group_query.setLayout(query_layout)
left_layout.addWidget(group_query)
left_layout.addStretch()
content_layout.addWidget(left_panel)
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
group_map = QGroupBox("4. GPV座標 リファレンスマップ")
map_layout = QVBoxLayout()
self.scene = QGraphicsScene()
self.view = ZoomableView(self.scene)
self.view.setStyleSheet("background-color: #E1E4E8; border: 1px solid #D0D7DE;")
map_layout.addWidget(self.view)
map_ctrl_layout = QHBoxLayout()
btn_reload = QPushButton("🔄 地図画像を再読み込み")
btn_reload.clicked.connect(self.reload_reference_map)
btn_select_map = QPushButton("📁 画像を変更")
btn_select_map.setStyleSheet("background-color: #6C757D;")
btn_select_map.clicked.connect(self.select_map_image)
map_ctrl_layout.addWidget(btn_reload)
map_ctrl_layout.addWidget(btn_select_map)
map_layout.addLayout(map_ctrl_layout)
group_map.setLayout(map_layout)
right_layout.addWidget(group_map)
content_layout.addWidget(right_panel, stretch=1)
main_layout.addLayout(content_layout)
self.setCentralWidget(main_widget)
def set_ui_locked(self, locked):
for chk, _ in self.chk_elements.values():
chk.setEnabled(not locked)
self.btn_recent24.setEnabled(not locked)
self.btn_range.setEnabled(not locked)
self.grib_date_start.setEnabled(not locked)
self.grib_date_end.setEnabled(not locked)
if locked:
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
else:
QApplication.restoreOverrideCursor()
def toggle_t8_auto(self):
if self.t8_auto_timer.isActive():
self.t8_auto_timer.stop()
self.lbl_t8_auto.setText("停止中")
self.btn_t8_auto.setText("▶ 最新1件を取得 & 自動監視(10分毎)を開始")
self.btn_t8_auto.setStyleSheet("background-color: #D73A49; color: #FFFFFF;")
else:
self.run_extraction_process("latest", silent=True)
self.t8_auto_timer.start(600000)
self.lbl_t8_auto.setText("🔴 自動取得中")
self.btn_t8_auto.setText("■ 自動監視を停止する")
self.btn_t8_auto.setStyleSheet("background-color: #6C757D; color: #FFFFFF;")
def run_extraction_process(self, mode, silent=False):
targets = []
for key, (chk, db_col) in self.chk_elements.items():
if chk.isChecked():
folder = self.grib_folders.get(key)
if folder and os.path.exists(folder):
targets.append((key, folder, db_col, chk.text()))
else:
if not silent: QMessageBox.warning(self, "警告", f"{chk.text()} のフォルダが未設定か存在しません。")
if not targets:
if not silent: QMessageBox.warning(self, "確認", "抽出対象の要素にチェックが入っていないか、フォルダが不正です。")
return
self.set_ui_locked(True)
self.lbl_status.setText("ステータス: ファイルを探索・処理準備中...")
jst_tz = timezone(timedelta(hours=9))
now_jst = datetime.now(jst_tz)
if mode == "recent24h":
start_dt_utc = (now_jst - timedelta(hours=24)).astimezone(timezone.utc)
end_dt_utc = now_jst.astimezone(timezone.utc)
elif mode == "range":
start_date_jst = self.grib_date_start.date().toPyDate()
end_date_jst = self.grib_date_end.date().toPyDate()
start_dt_utc = datetime.combine(start_date_jst, datetime.min.time(), tzinfo=jst_tz).astimezone(timezone.utc)
end_dt_utc = datetime.combine(end_date_jst, datetime.max.time(), tzinfo=jst_tz).astimezone(timezone.utc)
started_workers = 0
for key, folder, db_col, name in targets:
if key == "rain": pattern = "**/*Prr60lv_ANAL*.bin"
elif key == "snowfall": pattern = "**/*Psflv_ANAL*.bin"
else: pattern = "**/*Psdlv_ANAL*.bin"
bin_files = glob.glob(os.path.join(folder, pattern), recursive=True)
if not bin_files:
continue
def get_dt_from_f(f):
m = re.search(r'(20[2-9]\d{5,11})', os.path.basename(f))
if m:
ds = m.group(1)
if len(ds) >= 12: return ds[:12]
elif len(ds) == 10: return ds + "00"
elif len(ds) == 8: return ds + "0000"
return "000000000000"
bin_files.sort(key=get_dt_from_f)
target_files = []
if mode == "latest":
for f in reversed(bin_files):
dt_str = get_dt_from_f(f)
if dt_str.endswith("00") and dt_str != "000000000000":
target_files.append(f)
break
else:
for f in bin_files:
dt_str = get_dt_from_f(f)
if dt_str == "000000000000": continue
if not dt_str.endswith("00"): continue
dt_utc = datetime.strptime(dt_str, "%Y%m%d%H%M").replace(tzinfo=timezone.utc)
if start_dt_utc <= dt_utc <= end_dt_utc:
target_files.append(f)
if not target_files:
continue
worker = ExtractionWorker(target_files, db_col, self.db_path, self.wgrib2_path,
self.lat_min, self.lat_max, self.lon_min, self.lon_max)
worker.progress_signal.connect(lambda msg, n=name: self.lbl_status.setText(f"ステータス: {n} - {msg}"))
worker.finished_signal.connect(lambda count, n=name, s=silent, w=worker: self.on_extraction_finished(count, n, s, w))
worker.error_signal.connect(lambda err, w=worker: self.on_extraction_error(err, w))
self.workers.append(worker)
worker.start()
started_workers += 1
if started_workers == 0:
self.set_ui_locked(False)
if not silent: QMessageBox.warning(self, "該当なし", "指定条件に該当するGRIB2ファイルが見つかりませんでした。")
def on_extraction_finished(self, count, data_name, silent, worker):
if worker in self.workers: self.workers.remove(worker)
if not self.workers:
self.set_ui_locked(False)
self.lbl_status.setText("ステータス: すべての処理が完了・待機中")
self.update_t8_query_points()
if not silent:
QMessageBox.information(self, "完了", "要求された抽出処理がすべて完了しました。")
def on_extraction_error(self, err_msg, worker):
if worker in self.workers: self.workers.remove(worker)
if not self.workers:
self.set_ui_locked(False)
self.lbl_status.setText("ステータス: エラー発生 / 待機中")
QMessageBox.critical(self, "書き込みエラー", f"エラーが発生しました:\n{err_msg}")
def select_db(self):
path, _ = QFileDialog.getOpenFileName(self, "SQLite DBを選択", "", "SQLite DB (*.db);;All Files (*.*)")
if path:
self.db_path = os.path.abspath(path)
self.settings.setValue("last_db_path", self.db_path)
self.lbl_db_path.setText(f"🔗 統合共有DB: {self.db_path}\n(※非同期書込エンジン稼働中)")
self.update_t8_query_points()
def select_wgrib2(self):
path, _ = QFileDialog.getOpenFileName(self, "wgrib2.exe を選択", "", "Executable (*.exe);;All Files (*.*)")
if path:
self.wgrib2_path = os.path.abspath(path)
self.settings.setValue("wgrib2_path", self.wgrib2_path)
self.lbl_wgrib2_path.setText(self.wgrib2_path)
def select_grib_folder(self, key):
folder = QFileDialog.getExistingDirectory(self, "フォルダを選択")
if folder:
self.grib_folders[key] = folder
self.grib_labels[key].setText(f"📁 {folder}")
self.settings.setValue(f"folder_{key}", folder)
def select_map_image(self):
path, _ = QFileDialog.getOpenFileName(self, "地図画像を選択", "", "Images (*.png *.jpg *.jpeg *.bmp);;All Files (*.*)")
if path:
self.map_img_path = os.path.abspath(path)
self.settings.setValue("map_img_path", self.map_img_path)
self.reload_reference_map()
def reload_reference_map(self):
self.scene.clear()
pixmap = QPixmap(self.map_img_path)
if not pixmap.isNull():
self.scene.addPixmap(pixmap)
self.view.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
else:
self.scene.addText(f"地図画像が見つかりません:\n{self.map_img_path}")
def update_t8_query_points(self):
if not hasattr(self, 't8_q_point'): return
self.t8_q_point.clear()
try:
with sqlite3.connect(self.db_path, timeout=10.0) as conn:
cursor = conn.cursor()
cursor.execute("SELECT DISTINCT MsCd FROM OBS_GP ORDER BY MsCd")
points = cursor.fetchall()
cursor.execute("SELECT PointID, Lon, Lat, PointIDText FROM GPV_Points")
gpv_map = {}
for r in cursor.fetchall():
pid_text = r[3] if r[3] else str(r[0]).zfill(5)
gpv_map[f"GPV_{pid_text}"] = (r[1], r[2])
display_items = []
for p in points:
code = str(p[0])
if code in gpv_map:
lon, lat = gpv_map[code]
display_items.append(f"{code} (Lon: {lon:.3f}, Lat: {lat:.3f})")
else:
display_items.append(code)
self.t8_q_point.addItems(display_items)
except Exception: pass
def run_t8_simple_query(self):
date_str = self.t8_q_date.date().toString("yyyy-MM-dd")
time_str = self.t8_q_time.currentText()
if not self.t8_q_point.currentText():
self.lbl_t8_q_result.setText("結果: 地点未選択")
return
ms_cd = self.t8_q_point.currentText().split(" (")[0]
el_str = self.t8_q_element.currentText()
if "Rain" in el_str: col, unit = "Rainfall1h", "mm"
elif "Snowfall" in el_str: col, unit = "Snowfall1h", "cm"
else: col, unit = "SnowDepth", "cm"
try:
with sqlite3.connect(self.db_path, timeout=10.0) as conn:
cursor = conn.cursor()
if "日合計" in time_str:
jst_start = datetime.combine(self.t8_q_date.date().toPyDate(), datetime.min.time())
utc_start = jst_start - timedelta(hours=9)
utc_end = utc_start + timedelta(hours=23)
u_start_str = utc_start.strftime("%Y-%m-%d %H:%M")
u_end_str = utc_end.strftime("%Y-%m-%d %H:%M")
if col == "SnowDepth":
cursor.execute(f"SELECT MAX({col}) FROM OBS_GP WHERE ObsDay || ' ' || ObsTime BETWEEN ? AND ? AND MsCd=?", (u_start_str, u_end_str, ms_cd))
else:
cursor.execute(f"SELECT SUM({col}) FROM OBS_GP WHERE ObsDay || ' ' || ObsTime BETWEEN ? AND ? AND MsCd=?", (u_start_str, u_end_str, ms_cd))
else:
jst_dt = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
utc_dt = jst_dt - timedelta(hours=9)
cursor.execute(f"SELECT {col} FROM OBS_GP WHERE ObsDay=? AND ObsTime=? AND MsCd=?", (utc_dt.strftime("%Y-%m-%d"), utc_dt.strftime("%H:%M"), ms_cd))
row = cursor.fetchone()
if row and row[0] is not None:
self.lbl_t8_q_result.setText(f"結果: {row[0]:.1f} {unit}")
else:
self.lbl_t8_q_result.setText("結果: データなし")
except Exception:
self.lbl_t8_q_result.setText("結果: エラー")
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
QImageReader.setAllocationLimit(0)
window = GPVExtractorApp()
window.show()
sys.exit(app.exec())