未分類

    def update_slider_max(self, ui_key, init_time_str):
ui = self.tab_ui.get(ui_key)
if not ui: return
files = glob.glob(os.path.join(self.cache_dir, f"{ui_key}_{init_time_str}_FT*.npz"))
max_ft_found = max([0] + [int(re.search(r'_FT(\d+)\.npz', f).group(1)) for f in files if re.search(r'_FT(\d+)\.npz', f)])
hh = init_time_str[8:10] if len(init_time_str) >= 10 else '00'

# MSM_GUIDのstepを1から3に変更し、MSMとANAL以外は全て3の倍数固定へ
if ui_key == "MSM": default_max = 78 if hh in ['00', '12'] else 39; step = 1; tick_step = 6
elif ui_key == "GSM_JP": default_max = 264 if hh in ['00', '12'] else 132; step = 3; tick_step = 12
elif ui_key == "ANAL": default_max = 0; step = 1; tick_step = 1
elif ui_key == "MSM_GUID": default_max = 84; step = 3; tick_step = 6
elif ui_key == "GSM_GUID": default_max = 84; step = 3; tick_step = 12
elif ui_key in MEPS_STAT_MODELS: default_max = 39; step = 3; tick_step = 12
else: default_max = 132; step = 6; tick_step = 24

final_max = max(default_max, max_ft_found); final_max = (final_max // step) * step

ui['slider'].setMaximum(final_max)
ui['slider'].setTickInterval(step); ui['slider'].setSingleStep(step); ui['slider'].setPageStep(step)

ticks_layout = ui['ticks_layout']
for i in reversed(range(ticks_layout.count())):
w = ticks_layout.itemAt(i).widget()
if w: w.setParent(None)

if ui_key != "ANAL":
for t in range(0, final_max + 1, tick_step):
lbl = QLabel(f"{t}h"); lbl.setStyleSheet("font-size: 8pt; color: #8892B0;"); lbl.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter); ticks_layout.addWidget(lbl)

def create_compressed_left_panel(self, model_name):
left_panel = QVBoxLayout(); left_panel.setContentsMargins(5, 5, 5, 5)
# 3時間固定化の適用
step = 1 if model_name in ["MSM", "ANAL"] else 3

left_panel.addWidget(QLabel(f"📡 {model_name} 初期時刻(UTC):", styleSheet="font-weight: bold; font-size: 11pt; color: #E0E0E0;"))
init_combo = QComboBox(); init_combo.addItem("(データ待機中...)", None); left_panel.addWidget(init_combo)
interval_label = QLabel(f"【{step}時間間隔表示】", styleSheet="color: #64FFDA; font-weight: bold; font-size: 11pt;"); left_panel.addWidget(interval_label)

mid_hbox = QHBoxLayout(); elem_vbox = QVBoxLayout(); elem_vbox.addWidget(QLabel("■ 要素選択"))
element_list = QListWidget(); element_list.setFixedWidth(200)

if model_name in MEPS_STAT_MODELS:
elems = ["3時間降水量", "1時間最大降水量", "3時間最大降水量", "24時間最大降水量", "3時間降雪量", "6時間降雪量", "12時間降雪量", "24時間降雪量", "発雷確率"]
elif "GUID" in model_name:
elems = ["卓越天気", "3時間降水量", "6時間降水量", "12時間降水量", "24時間降水量", "3時間降雪量", "6時間降雪量", "12時間降雪量", "24時間降雪量", "発雷確率"]
elif model_name == "ANAL":
elems = ["地上気温", "地上風向風速", "300hPa風向・風速", "500hPa風向・風速", "500hPa気温", "700hPa気温・風", "850hPa気温・風", "925hPa気温・風", "975hPa気温・風"]
else:
elems = ["地上3種重ね", "地上気圧","地上風向風速", "地上降水", "全雲量", "上層雲量", "中層雲量", "下層雲量", "300hPa高度", "300hPa風向・風速", "500hPa高度", "500hPa風向・風速", "500hPa渦度", "500hPa気温", "700hPa湿数・風", "700hPa鉛直流", "850hPa気温・風", "850hPa相当温位", "850hPa湿数・風", "925hPa相当温位", "925hPa湿数・風", "975hPa相当温位", "975hPa湿数・風"]

element_list.addItems(elems); element_list.setCurrentRow(0); elem_vbox.addWidget(element_list); mid_hbox.addLayout(elem_vbox)

slider_vbox = QVBoxLayout()
time_label = QLabel("FT=0h\n(--/--)"); time_label.setStyleSheet("font-weight: bold; color: #64FFDA; font-size: 11pt; background: #1D3557; padding: 5px; border: 2px solid #457B9D; border-radius: 5px;"); time_label.setAlignment(Qt.AlignmentFlag.AlignCenter); slider_vbox.addWidget(time_label)

slider_control_hbox = QHBoxLayout()
btn_prev = QPushButton("▲"); btn_prev.setProperty("class", "NavButton"); btn_prev.setFixedWidth(30)
btn_next = QPushButton("▼"); btn_next.setProperty("class", "NavButton"); btn_next.setFixedWidth(30)
slider = QSlider(Qt.Orientation.Vertical); slider.setRange(0, 39); slider.setTickPosition(QSlider.TickPosition.TicksLeft); slider.setTickInterval(step); slider.setSingleStep(step); slider.setPageStep(step); slider.setInvertedAppearance(True)

btn_vbox = QVBoxLayout(); btn_vbox.addWidget(slider, stretch=1); btn_vbox.setAlignment(Qt.AlignmentFlag.AlignHCenter)
nav_hbox = QHBoxLayout(); nav_hbox.addWidget(btn_prev); nav_hbox.addWidget(btn_next)
inner_slider_vbox = QVBoxLayout(); inner_slider_vbox.addLayout(nav_hbox); inner_slider_vbox.addLayout(btn_vbox); slider_control_hbox.addLayout(inner_slider_vbox)

ticks_vbox = QVBoxLayout(); ticks_vbox.setContentsMargins(0, 30, 0, 10)
slider_control_hbox.addLayout(ticks_vbox); slider_vbox.addLayout(slider_control_hbox, stretch=1)

play_hbox = QHBoxLayout()
btn_play = QPushButton("▶"); btn_play.setProperty("class", "PlayButton"); btn_play.setFixedWidth(40)
btn_stop = QPushButton("■"); btn_stop.setProperty("class", "StopButton"); btn_stop.setFixedWidth(40)
play_hbox.addWidget(btn_play); play_hbox.addWidget(btn_stop); slider_vbox.addLayout(play_hbox)

combo_speed = QComboBox(); combo_speed.addItems(["速1", "速2", "速3", "速4", "速5"]); combo_speed.setCurrentIndex(2); slider_vbox.addWidget(combo_speed)
mid_hbox.addLayout(slider_vbox); left_panel.addLayout(mid_hbox, stretch=1)

return left_panel, init_combo, interval_label, element_list, time_label, btn_prev, btn_next, slider, btn_play, btn_stop, combo_speed, ticks_vbox

def setup_tab(self, model_name, tab_title, is_guidance=False, is_anal=False):
tab = QWidget(); main_hbox = QHBoxLayout(tab)
left_panel, init_combo, interval_label, element_list, time_label, btn_prev, btn_next, slider, btn_play, btn_stop, combo_speed, ticks_layout = self.create_compressed_left_panel(model_name)
main_hbox.addLayout(left_panel, stretch=2)

right_panel = QVBoxLayout()
area_layout = QHBoxLayout(); area_layout.addWidget(QLabel("表示領域:"))

area_btns = [QPushButton("日本周辺"), QPushButton("北海道全域"), QPushButton("上川地方"), QPushButton("札幌近郊"), QPushButton("恵庭・石狩"), QPushButton("📊 メタグラム")]
for i, btn in enumerate(area_btns):
btn.setCheckable(True); area_layout.addWidget(btn); btn.clicked.connect(lambda checked, idx=i, m=model_name: self.on_area_btn_clicked(m, idx))
area_btns[0].setChecked(True)

if not is_anal:
btn_prev.clicked.connect(lambda: step_slider(-1)); btn_next.clicked.connect(lambda: step_slider(1))
slider.valueChanged.connect(lambda val, m=model_name: self.on_slider_changed(m, val))
btn_play.clicked.connect(lambda: self.start_animation(model_name)); btn_stop.clicked.connect(self.stop_animation)
element_list.itemSelectionChanged.connect(lambda m=model_name: self.on_element_changed(m))
init_combo.currentIndexChanged.connect(lambda idx, m=model_name: self.on_init_changed(m, idx))
self.tabs.addTab(tab, tab_title)

area_layout.addStretch()
right_panel.addLayout(area_layout)

fig = Figure(figsize=(14, 13)); fig.patch.set_facecolor('white'); canvas = FigureCanvas(fig)

meta_ctrl_widget = QWidget(); meta_ctrl_layout = QVBoxLayout(meta_ctrl_widget)
h_ctrl = QHBoxLayout()
h_ctrl.addWidget(QLabel("地点選択:")); station_combo = QComboBox()
for name, lat, lon in STATIONS: station_combo.addItem(name, (lat, lon))
h_ctrl.addWidget(station_combo)

if not is_anal:
btn_text = "表示 (A4横サイズ テーブル画像生成)" if is_guidance else "表示 (図解メタグラム生成)"
btn_generate_meta = QPushButton(btn_text)
btn_generate_meta.setProperty("class", "SyncButton")
if is_guidance or model_name in MEPS_STAT_MODELS: btn_generate_meta.clicked.connect(lambda: self.draw_guidance_metagram(station_combo, model_name))
elif model_name == "GSM_JP": btn_generate_meta.clicked.connect(lambda: self.draw_gsm_visual_metagram(station_combo))
elif model_name == "MSM": btn_generate_meta.clicked.connect(lambda: self.draw_msm_visual_metagram(station_combo))
h_ctrl.addWidget(btn_generate_meta)

h_ctrl.addStretch()
meta_ctrl_layout.addLayout(h_ctrl); meta_ctrl_layout.addStretch(); meta_ctrl_widget.hide()

right_panel.addWidget(canvas, stretch=10)
right_panel.addWidget(meta_ctrl_widget, stretch=10)
main_hbox.addLayout(right_panel, stretch=8)

info_panel = QLabel(canvas)
info_panel.setStyleSheet("background-color: rgba(17, 34, 64, 0.85); border: 2px solid #64FFDA; border-radius: 5px; padding: 5px; font-weight: bold; font-size: 12pt; color: #E0E0E0;"); info_panel.hide()

# 3時間固定化の適用
step = 1 if model_name in ["MSM", "ANAL"] else 3

self.tab_ui[model_name] = {
'list': element_list, 'area_btns': area_btns, 'slider': slider, 'time_label': time_label, 'init_combo': init_combo,
'interval_label': interval_label, 'fig': fig, 'canvas': canvas, 'meta_ctrl': meta_ctrl_widget, 'ax': None, 'current_area_index': -1,
'dynamic_artists': [], 'colorbars': [], 'info_panel': info_panel, 'combo_speed': combo_speed, 'ticks_layout': ticks_layout,
'step': step,'last_val':0
}

if is_anal:
btn_play.hide(); btn_stop.hide(); combo_speed.hide()
btn_prev.hide(); btn_next.hide()
slider.hide()
for i in range(ticks_layout.count()):
w = ticks_layout.itemAt(i).widget()
if w: w.hide()
interval_label.setText("【現況解析のみ(FT=0)】")

def step_slider(val_change, m=model_name):
ui = self.tab_ui[m]
current_val = ui['slider'].value()
elem_name = ui['list'].currentItem().text()

s_step = ui['step']
if m == "GSM_JP":
ground_cloud_elems = ["地上気圧", "地上風向風速", "地上降水", "全雲量", "上層雲量", "中層雲量", "下層雲量", "地上3種重ね"]
threshold = 132 if elem_name in ground_cloud_elems else 84
if val_change > 0: s_step = 6 if current_val >= threshold else 3
else: s_step = 6 if current_val > threshold else 3

new_val = current_val + (s_step * val_change)
if new_val > ui['slider'].maximum(): new_val = ui['slider'].minimum()
if new_val < ui['slider'].minimum(): new_val = ui['slider'].maximum()
ui['slider'].setValue(new_val)
    def draw_guidance_metagram(self, station_combo, model_name):
it_str = self.model_init_times.get(model_name)
if not it_str:
QMessageBox.warning(self, "エラー", "データが存在しません。"); return

stat_name = station_combo.currentText(); stat_lat, stat_lon = station_combo.currentData()
dt_utc = datetime.strptime(it_str, "%Y%m%d%H%M%S")
dt_jst = dt_utc + timedelta(hours=9)

is_meps = model_name in MEPS_STAT_MODELS

if is_meps:
elements = [
("3時間降水量", "precip", 1),
("1時間最大\n降水量", "precip1max", 1),
("3時間最大\n降水量", "precip3max", 1),
("24時間最大\n降水量", "precip24max", 8),
("3時間降雪量", "snow3", 1),
("6時間降雪量", "snow6", 2),
("12時間降雪量", "snow12", 4),
("24時間降雪量", "snow24", 8),
("発雷確率", "thund", 1)
]

stats = [
("上位90%", "MEPS_GUID_MAX"),
("確率≥1", "MEPS_GUID_PROB1"),
("確率≥10", "MEPS_GUID_PROB10"),
("確率≥20", "MEPS_GUID_PROB20"),
("確率≥30", "MEPS_GUID_PROB30"),
("下位10%", "MEPS_GUID_MIN")
]

max_ft = 39
fts = list(range(3, max_ft + 1, 3))

data_dict = {elem[1]: {s[0]: {} for s in stats} for elem in elements}

for s_name, s_model in stats:
s_it = self.model_init_times.get(s_model, it_str)
for ft in fts:
guid_file = os.path.join(self.cache_dir, f"{s_model}_{s_it}_FT{ft:02d}.npz")
if os.path.exists(guid_file):
try:
with np.load(guid_file) as d:
for e_name, e_key, span in elements:
search_keys = [e_key]
span_h = span * 3 if span < 8 else 24

# 動的に代替キー(fallback key)を検索して完全抽出
if "降水量" in e_name:
if "最大" in e_name:
span_h = 1 if "1時間" in e_name else (3 if "3時間" in e_name else 24)
search_keys.extend([k for k in d.files if k.startswith("tp_") and k.endswith(f"_{max(0, ft-span_h)}_{ft}")])
else:
search_keys.extend(['tp', 'precip'] + [k for k in d.files if k.startswith("tp_") and k.endswith(f"_{max(0, ft-3)}_{ft}")])
elif "降雪" in e_name:
search_keys.extend(['asnow'] + [k for k in d.files if k.startswith("asnow_") and k.endswith(f"_{max(0, ft-span_h)}_{ft}")])
elif "発雷" in e_name:
search_keys.extend(['tstm', 'thund'] + [k for k in d.files if k.startswith("tstm_") and k.endswith(f"_{max(0, ft-3)}_{ft}")])

val = self.extract_val_robust(d, search_keys, stat_lon, stat_lat)
data_dict[e_key][s_name][ft] = val
except: pass

fig = Figure(figsize=(16, 12)); fig.patch.set_facecolor('white')
fig.subplots_adjust(left=0.01, right=0.99, top=0.94, bottom=0.01)
ax = fig.add_subplot(111); ax.axis('off')

n_rows = len(elements) * len(stats) + 2
n_cols = 2 + len(fts)

ax.set_xlim(0, n_cols)
ax.set_ylim(0, n_rows)

def draw_cell(x, y, w, h, text, bg_color='white', font_weight='normal', text_color='black'):
rect = plt.Rectangle((x, y), w, h, facecolor=bg_color, edgecolor='black', linewidth=0.5)
ax.add_patch(rect)
ax.text(x + w/2.0, y + h/2.0, text, color=text_color, ha='center', va='center', weight=font_weight, fontsize=13)

current_y = n_rows - 1
draw_cell(0, current_y - 1, 2, 2, "要素 / 日時(JST)", bg_color='#1D3557', font_weight='bold', text_color='white')

for i, ft in enumerate(fts):
target_jst = dt_jst + timedelta(hours=ft)
draw_cell(2 + i, current_y, 1, 1, target_jst.strftime("%m/%d"), bg_color='#1D3557', font_weight='bold', text_color='white')
draw_cell(2 + i, current_y - 1, 1, 1, f"{target_jst.strftime('%H')}時\n(FT{ft:02d})", bg_color='#457B9D', font_weight='bold', text_color='white')

current_y -= 2

for e_name, e_key, span in elements:
draw_cell(0, current_y - len(stats) + 1, 1, len(stats), e_name, bg_color='#F1FAEE', font_weight='bold')

for s_idx, (s_name, _) in enumerate(stats):
row_y = current_y - s_idx
draw_cell(1, row_y, 1, 1, s_name, bg_color='#E0E0E0' if s_idx%2==0 else 'white')

col_idx = 0
while col_idx < len(fts):
ft = fts[col_idx]
target_ft = ft + (span - 1) * 3
val = data_dict[e_key][s_name].get(target_ft, np.nan)

is_prob_stat = "確率" in s_name

val_str = f"{val:.1f}" if not np.isnan(val) else "-"
if ("確率" in e_name or is_prob_stat) and not np.isnan(val):
val_str = f"{val:.0f}"
if val_str == "0.0" or val_str == "0":
val_str = "0"
if is_prob_stat and val_str != "-" and val_str != "0":
val_str += "%"

w = min(span, len(fts) - col_idx)
bg_c = 'white' if s_idx%2!=0 else '#F8F9FA'

if not np.isnan(val) and val > 0:
if is_prob_stat or "確率" in e_name:
if val >= 50: bg_c = '#FFCDD2'
elif val >= 20: bg_c = '#FFF9C4'
elif "降水" in e_name or "降雪" in e_name:
if val >= 20: bg_c = '#FFCDD2'
elif val >= 10: bg_c = '#FFF9C4'
elif val >= 1: bg_c = '#E3F2FD'

draw_cell(2 + col_idx, row_y, w, 1, val_str, bg_color=bg_c)
col_idx += span

current_y -= len(stats)

fig.suptitle(f"{stat_name} MEPS 統合メタグラム (初期時: {dt_utc.strftime('%m/%d %H:00Z')})", fontsize=20, weight='bold', y=0.99)
canvas = FigureCanvas(fig)
win = SingleImageWindow(self, f"GUID_{stat_name}", model_name, stat_name, dt_utc.strftime("%m%d%H") + "Z", canvas)
win.exec()

else:
fig = Figure(figsize=(11.69, 8.27)); fig.patch.set_facecolor('white')
ax = fig.add_subplot(111); ax.axis('off')

table_data = []
headers = ["FT", "日時(JST)", "卓越天気", "降水量", "降雪量", "発雷確率", "気温(℃)", "風向", "風速(m/s)"]

max_ft = self.tab_ui[model_name]['slider'].maximum()
fts = list(range(0, max_ft + 1, 3))

gpv_model = "GSM_JP" if model_name == "GSM_GUID" else "MSM"
gpv_it_str = self.model_init_times.get(gpv_model)

for ft in fts:
target_jst = dt_utc + timedelta(hours=9+ft)
row = [f"FT={ft:02d}", target_jst.strftime("%m/%d %H:00"), "-", "-", "-", "-", "-", "-", "-"]

guid_file = os.path.join(self.cache_dir, f"{model_name}_{it_str}_FT{ft:02d}.npz")
if os.path.exists(guid_file):
try:
with np.load(guid_file) as d:
pop = self.extract_val_robust(d, ['precip', 'tp', 'apcp', 'pop', 'var_0_1_8', 'var_0_1_52'], stat_lon, stat_lat)
pos = self.extract_val_robust(d, ['snow', 'weasd', 'snod', 'var_0_1_11', 'var_0_1_13', 'var_0_1_29'], stat_lon, stat_lat)
pol = self.extract_val_robust(d, ['thund', 'pol', 'lig', 'thunder', 'tstm', 'var_0_19_193'], stat_lon, stat_lat)
wx = self.extract_val_robust(d, ['wea', 'weather', 'wx', 'var_-1_-1_-1', 'var_0_19_192'], stat_lon, stat_lat)

if not np.isnan(wx): row[2] = {1:"晴", 2:"曇", 3:"雨", 4:"雪"}.get(int(wx), "不明")
if not np.isnan(pop): row[3] = f"{pop:.1f} mm"
if not np.isnan(pos): row[4] = f"{pos:.1f} cm"
if not np.isnan(pol): row[5] = f"{pol:.0f} %"
except: pass

gpv_ft = ft
if gpv_model == "GSM_JP" and ft > 132: gpv_ft = ft - (ft % 6) if ft % 6 != 0 else ft

gpv_file = os.path.join(self.cache_dir, f"{gpv_model}_{gpv_it_str}_FT{gpv_ft:02d}.npz")
if not os.path.exists(gpv_file): gpv_file = os.path.join(self.cache_dir, f"{gpv_model}_{it_str}_FT{gpv_ft:02d}.npz")
if not os.path.exists(gpv_file):
p_files = glob.glob(os.path.join(self.cache_dir, f"{gpv_model}_*_FT{gpv_ft:02d}.npz"))
if p_files: gpv_file = sorted(p_files, key=lambda x: abs(int(re.search(r'_(\d{14})_', x).group(1)) - int(it_str)))[0]

if os.path.exists(gpv_file):
try:
with np.load(gpv_file) as d_gpv:
t = self.extract_val_robust(d_gpv, ['t2m', 'tmp2m', 'temp2m'], stat_lon, stat_lat)
if not np.isnan(t): row[6] = f"{(t - 273.15 if t > 100 else t):.1f}"
u = self.extract_val_robust(d_gpv, ['u10', '10u', 'u_10m'], stat_lon, stat_lat)
v = self.extract_val_robust(d_gpv, ['v10', '10v', 'v_10m'], stat_lon, stat_lat)
if not np.isnan(u) and not np.isnan(v):
row[8] = f"{np.hypot(u, v):.1f}"
deg = (np.degrees(np.arctan2(u, v)) + 180) % 360
dirs = ["北", "北北東", "北東", "東北東", "東", "東南東", "南東", "南南東", "南", "南南西", "南西", "西南西", "西", "西北西", "北西", "北北西", "北"]
row[7] = dirs[int(round(deg / 22.5)) % 16]
except: pass

table_data.append(row)

table = ax.table(cellText=table_data, colLabels=headers, loc='center', cellLoc='center')
table.auto_set_font_size(False); table.set_fontsize(10); table.scale(1, 1.4)

for j in range(len(headers)):
cell = table[0, j]; cell.set_facecolor('#1D3557'); cell.get_text().set_color('white'); cell.get_text().set_weight('bold')

fig.suptitle(f"{stat_name} 統合ガイダンス時系列表 (初期時: {dt_utc.strftime('%m/%d %H:00Z')})", fontsize=15, weight='bold', y=0.96)
canvas = FigureCanvas(fig)
win = SingleImageWindow(self, f"GUID_{stat_name}", model_name, stat_name, dt_utc.strftime("%m%d%H") + "Z", canvas)
win.exec()

def draw_guidance(self, ax, ui, data, elem_name, val, extent, area_index, model_name, it_str):
drawn_flag = False; info_text = ""
is_meps = model_name in MEPS_STAT_MODELS
is_prob = "PROB" in model_name

def _cl(arr, minv, maxv, fill=np.nan):
if arr is None: return None
a = np.array(arr, dtype=float)
if not np.isnan(fill): a = np.nan_to_num(a, nan=fill, posinf=fill, neginf=fill)
else: a[np.isinf(a)] = np.nan
return np.clip(a, minv, maxv)

def add_vertical_colorbar(mappable, label_text):
cax = ui['fig'].add_axes([0.93, 0.15, 0.018, 0.7])
cb = ui['fig'].colorbar(mappable, cax=cax, orientation='vertical')
cb.set_label(label_text, fontsize=12, weight='bold'); ui['colorbars'].append(cb)

if "降水量" in elem_name or "降雪量" in elem_name:
accum_val = None; valid_data = False; lon_c = None; lat_c = None

if is_meps:
if "1時間" in elem_name: accum_hours = 1
elif "3時間" in elem_name: accum_hours = 3
elif "6時間" in elem_name: accum_hours = 6
elif "12時間" in elem_name: accum_hours = 12
elif "24時間" in elem_name: accum_hours = 24
else: accum_hours = 3

fixed_key = None
if "降水量" in elem_name:
if "1時間最大" in elem_name: fixed_key = "precip1max"
elif "3時間最大" in elem_name: fixed_key = "precip3max"
elif "24時間最大" in elem_name: fixed_key = "precip24max"
else: fixed_key = "precip"
else:
fixed_key = f"snow{accum_hours}"

meps_key = None
if fixed_key and fixed_key in data.files:
meps_key = fixed_key
else:
vc = "tp_" if "降水量" in elem_name else "asnow_"
tr = f"_{max(0, val-accum_hours)}_{val}"
for k in data.files:
if k.startswith(vc) and k.endswith(tr) and not ("lon" in k or "lat" in k):
meps_key = k; break
if not meps_key:
for k in data.files:
if k.startswith(vc) and k.endswith(f"_{val}") and not ("lon" in k or "lat" in k):
meps_key = k; break

if meps_key and meps_key in data.files:
accum_val = _cl(data[meps_key], 0, 1000, 0.0)
lon_c, lat_c = self.get_coords_for_data(data, accum_val)
valid_data = True

else:
accum_hours = 3
if "6時間" in elem_name: accum_hours = 6
elif "12時間" in elem_name: accum_hours = 12
elif "24時間" in elem_name: accum_hours = 24

for hr_offset in range(0, accum_hours, 3):
target_ft = val - hr_offset
if target_ft <= 0: continue

d_curr = None
if target_ft == val: d_curr = data
else:
target_file = os.path.join(self.cache_dir, f"{model_name}_{it_str}_FT{target_ft:02d}.npz")
if os.path.exists(target_file):
try: d_curr = np.load(target_file)
except: pass

if d_curr is not None:
if "降水量" in elem_name: key = self.get_exact_key(d_curr, ['precip', 'tp', 'apcp', 'pop', 'var_0_1_8', 'var_0_1_52'])
else: key = self.get_exact_key(d_curr, ['snow', 'weasd', 'snod', 'var_0_1_11', 'var_0_1_13', 'var_0_1_29', 'asnow'])
if key:
v = _cl(d_curr[key], 0, 1000, 0.0)
if accum_val is None:
accum_val = v.copy()
lon_c, lat_c = self.get_coords_for_data(d_curr, v)
valid_data = True
else:
accum_val += v

if target_ft != val: d_curr.close()

if valid_data and accum_val is not None:
accum_val_masked = np.ma.masked_less(accum_val, 1.0 if is_prob else 0.1)

if is_prob:
thresh = model_name.replace("MEPS_GUID_PROB", "")
unit = "mm" if "降水" in elem_name else "cm"
label_text = f'{elem_name} (≥{thresh}{unit}) 超過確率 [%]'
levels = [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
cmap = mcolors.ListedColormap(['#F7DC6F', '#F4D03F', '#F5B041', '#EB984E', '#DC7633', '#E74C3C', '#C0392B', '#9B59B6', '#8E44AD', '#1F618D'])
cmap.set_under('none'); cmap.set_over('#0B5345')
else:
levels = [0.1, 1, 5, 10, 20, 30, 50, 80]
if "降水量" in elem_name:
cmap = mcolors.ListedColormap(['#A0E8A0', '#D4F05A', '#FFFF00', '#FFA500', '#FF5500', '#FF0000', '#FF00FF'])
cmap.set_under('none'); cmap.set_over('#800080')
label_text = f'降水量 ({accum_hours}時間{"最大" if is_meps and "最大" in elem_name else "積算"}) [mm]'
else:
cmap = mcolors.ListedColormap(['#81D4FA', '#29B6F6', '#0288D1', '#1565C0', '#3F51B5', '#5E35B1', '#4A148C'])
cmap.set_under('none'); cmap.set_over('#311B92')
label_text = f'降雪量 ({accum_hours}時間積算) [cm等]'

norm = mcolors.BoundaryNorm(levels, cmap.N)
pm = self.plot_blocky(ax, lon_c, lat_c, accum_val_masked, ui, cmap, extent, norm=norm, alpha=0.55 if is_prob else 0.45)

if pm:
add_vertical_colorbar(pm, label_text)
drawn_flag = True

lc, lac, dc = self.crop_data(lon_c, lat_c, accum_val, extent)
if lc.size > 0:
l2, la2 = np.meshgrid(lc, lac) if lc.ndim == 1 else (lc, lac)
min_lon, max_lon, min_lat, max_lat = extent

if area_index in [3, 4]:
try:
zoom_factor = 8 if "GSM" in model_name else 2
dc_z = ndimage.zoom(dc, zoom_factor, order=1)
if lc.ndim == 1:
lc_z = ndimage.zoom(lc, zoom_factor, order=1)
lac_z = ndimage.zoom(lac, zoom_factor, order=1)
l2_z, la2_z = np.meshgrid(lc_z, lac_z)
else:
l2_z = ndimage.zoom(l2, zoom_factor, order=1)
la2_z = ndimage.zoom(la2, zoom_factor, order=1)

for r in range(dc_z.shape[0]):
for c in range(dc_z.shape[1]):
if r % zoom_factor == 0 and c % zoom_factor == 0: continue
lon_v, lat_v, val_p = l2_z[r, c], la2_z[r, c], dc_z[r, c]
limit = 1.0 if is_prob else 0.1
if val_p >= limit and min_lon <= lon_v <= max_lon and min_lat <= lat_v <= max_lat:
txt_fmt = f"{val_p:.0f}" if is_prob else f"{val_p:.1f}"
txt = ax.text(lon_v, lat_v, txt_fmt, color='#1f77b4', fontsize=10, ha='center', va='center', transform=ccrs.PlateCarree(), zorder=12, weight='bold', clip_on=True, path_effects=[pe.withStroke(linewidth=2, foreground='white')])
ui['dynamic_artists'].append(txt)

for r in range(dc.shape[0]):
for c in range(dc.shape[1]):
lon_v, lat_v, val_p = l2[r, c], la2[r, c], dc[r, c]
limit = 1.0 if is_prob else 0.1
if val_p >= limit and min_lon <= lon_v <= max_lon and min_lat <= lat_v <= max_lat:
txt_fmt = f"{val_p:.0f}" if is_prob else f"{val_p:.1f}"
txt = ax.text(lon_v, lat_v, txt_fmt, color='black', fontsize=11, ha='center', va='center', transform=ccrs.PlateCarree(), zorder=13, weight='bold', bbox=dict(facecolor='white', edgecolor='black', boxstyle='square,pad=0.2', alpha=0.9), clip_on=True)
ui['dynamic_artists'].append(txt)
except Exception as e:
print(f"札幌近郊 内挿・格子点描画エラー: {e}")

elif area_index == 2:
try:
zoom_factor = 4 if "GSM" in model_name else 1
dc_z = ndimage.zoom(dc, zoom_factor, order=1)
l2_z = ndimage.zoom(l2, zoom_factor, order=1)
la2_z = ndimage.zoom(la2, zoom_factor, order=1)
for r in range(dc_z.shape[0]):
for c in range(dc_z.shape[1]):
if r % zoom_factor == 0 and c % zoom_factor == 0:
lon_v, lat_v, val_p = l2_z[r, c], la2_z[r, c], dc_z[r, c]
limit = 1.0 if is_prob else 0.1
if val_p >= limit and min_lon <= lon_v <= max_lon and min_lat <= lat_v <= max_lat:
txt_fmt = f"{val_p:.0f}" if is_prob else f"{val_p:.1f}"
txt = ax.text(lon_v, lat_v, txt_fmt, color='black', fontsize=7, ha='center', va='center', transform=ccrs.PlateCarree(), zorder=13, weight='bold', bbox=dict(facecolor='white', edgecolor='black', boxstyle='square,pad=0.15', alpha=0.85), clip_on=True)
ui['dynamic_artists'].append(txt)
except Exception as e:
print(f"上川地方 透過文字描画エラー: {e}")

elif "発雷" in elem_name:
key = None
if is_meps:
if "thund" in data.files:
key = "thund"
else:
tr = f"_{max(0, val-3)}_{val}"
for k in data.files:
if k.startswith("tstm_") and k.endswith(tr) and not ("lon" in k or "lat" in k):
key = k; break
if not key:
for k in data.files:
if k.startswith("tstm_") and k.endswith(f"_{val}") and not ("lon" in k or "lat" in k):
key = k; break
else:
for k in data.files:
if any(x in k.lower() for x in ['thund', 'pol', 'lig', 'ts', 'tstm', 'var_0_19_193']) and not ("lon" in k or "lat" in k):
key = k; break

if key:
try:
val_data = _cl(data[key], 0, 100, np.nan)
lon_c, lat_c = self.get_coords_for_data(data, val_data)

if lon_c is None or lat_c is None:
lon_k = next((k for k in data.files if 'lon' in k.lower()), None)
lat_k = next((k for k in data.files if 'lat' in k.lower()), None)
if lon_k and lat_k: lon_c, lat_c = data[lon_k], data[lat_k]

if lon_c is not None and lat_c is not None:
if lon_c.ndim == 1 and val_data.shape == (len(lon_c), len(lat_c)): val_data = val_data.T
if lon_c.ndim == 1 and val_data.shape != (len(lat_c), len(lon_c)):
if val_data.shape[1] > val_data.shape[0] and len(lat_c) > len(lon_c): val_data = val_data.T
lon_c = np.linspace(lon_c.min(), lon_c.max(), val_data.shape[1])
lat_c = np.linspace(lat_c.min(), lat_c.max(), val_data.shape[0])

val_masked = np.ma.masked_less(val_data, 1.0)
levels = [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
cmap = mcolors.ListedColormap(['#F7DC6F', '#F4D03F', '#F5B041', '#EB984E', '#DC7633', '#E74C3C', '#C0392B', '#9B59B6', '#8E44AD', '#1F618D'])
cmap.set_under('none'); cmap.set_bad('none')
norm = mcolors.BoundaryNorm(levels, cmap.N)

pm = self.plot_blocky(ax, lon_c, lat_c, val_masked, ui, cmap, extent, norm=norm, alpha=0.6)
if pm:
add_vertical_colorbar(pm, '発雷確率凡例 (1%~100%) [%]')
info_text = self.get_local_info(elem_name, lon_c, lat_c, val_data, area_index=area_index, model_name=model_name)
drawn_flag = True

lc, lac, dc = self.crop_data(lon_c, lat_c, val_data, extent)
# 日本周辺(area_index == 0)では発雷数字を描画しないように除外
if lc.size > 0 and area_index != 0:
l2, la2 = np.meshgrid(lc, lac) if lc.ndim == 1 else (lc, lac)
min_lon, max_lon, min_lat, max_lat = extent
for r in range(dc.shape[0]):
for c in range(dc.shape[1]):
lon_v, lat_v, val_p = l2[r, c], la2[r, c], dc[r, c]
if val_p >= 1.0 and min_lon <= lon_v <= max_lon and min_lat <= lat_v <= max_lat:
txt = ax.text(lon_v, lat_v, f"{val_p:.0f}", color='black', fontsize=9, ha='center', va='center', transform=ccrs.PlateCarree(), zorder=13, weight='bold')
ui['dynamic_artists'].append(txt)
except Exception as e: print(f"Lightning graphic render exception: {e}")

elif "天気" in elem_name:
key = None
for k in data.files:
if any(x in k.lower() for x in ['wea', 'wx', 'var_-1_-1_-1', 'var_0_19_192']): key = k; break

if key:
val_data = _cl(data[key], 0, 10, np.nan)
lon_c, lat_c = self.get_coords_for_data(data, val_data)

if lon_c is None or lat_c is None:
lon_k = next((k for k in data.files if 'lon' in k.lower()), None)
lat_k = next((k for k in data.files if 'lat' in k.lower()), None)
if lon_k and lat_k: lon_c, lat_c = data[lon_k], data[lat_k]

if lon_c is not None and lat_c is not None:
if lon_c.ndim == 1 and val_data.shape == (len(lon_c), len(lat_c)):
val_data = val_data.T

val_masked = np.ma.masked_outside(val_data, 0.5, 4.5)
levels = [0.5, 1.5, 2.5, 3.5, 4.5]
colors = ['#FF9800', '#B0C4DE', '#1E90FF', '#FFFFFF']
cmap_wx = mcolors.ListedColormap(colors)
cmap_wx.set_under('none')
cmap_wx.set_over('none')
cmap_wx.set_bad('none')
norm_wx = mcolors.BoundaryNorm(levels, cmap_wx.N)

pm = self.plot_blocky(ax, lon_c, lat_c, val_masked, ui, cmap_wx, extent, norm=norm_wx, alpha=0.6)
if pm:
add_vertical_colorbar(pm, '卓越天気 (1:晴 2:曇 3:雨 4:雪)')
info_text = self.get_local_info(elem_name, lon_c, lat_c, val_data, area_index=area_index, model_name=model_name)
drawn_flag = True

return drawn_flag, info_text