"""L08 河川浸水 × 津波浸水 × 大規模盛土 — 複合災害リスクの重ね合わせ

3 層 (河川計画規模 / 河川想定最大規模 / 津波) を同一マップに重ね、
"ダブル浸水リスク" "トリプル浸水リスク" 領域を可視化する研究教材。

要件S 対策:
    - 津波 1,256,706 メッシュは事前に rank 8 段階に bin 化 → dissolve し、
      ``lessons/assets/L08_tsunami_dissolved.gpkg`` にキャッシュ。
      初回のみ ~50 秒、2 回目以降は <1 秒で再利用。
    - 本スクリプト本体は 1〜3 分以内で完走。

実行:
    cd "2026 DoBoX 教材"
    py -X utf8 lessons/L08_multi_flood_overlay.py
"""
from __future__ import annotations
import sys, time
from pathlib import Path
import zipfile

sys.path.insert(0, str(Path(__file__).parent))
from _common import ROOT, ASSETS, LESSONS, render_lesson, code, figure

import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
import geopandas as gpd
import shapely

plt.rcParams["font.family"] = "Yu Gothic"
plt.rcParams["axes.unicode_minus"] = False

t0 = time.time()
print("=== L08 河川 × 津波 × 盛土 複合災害 ===")

TARGET_CRS = "EPSG:6671"  # JGD2011 Plane Rectangular III (m)

# ── 浸水深 8 段階 共通凡例 (国交省/広島県ガイドラインに準拠) ─────────────
DEPTH_LABEL = {
    10: "0.0〜0.5m", 20: "0.5〜1.0m", 30: "1.0〜2.0m", 40: "2.0〜3.0m",
    50: "3.0〜5.0m", 60: "5.0〜10.0m", 70: "10.0〜20.0m", 80: "20m以上",
}
DEPTH_COLOR = {
    10: "#bee2ff", 20: "#87c4f0", 30: "#56a4dc", 40: "#2c83c4",
    50: "#1c63a4", 60: "#0e4282", 70: "#7d2cbf", 80: "#4a1280",
}

# === 1. 河川 浸水想定 Shapefile (rank 列が浸水深ランク) ====================
print("\n[1] 河川浸水想定 Shapefile 読み込み")
t1 = time.time()
FLOOD_DIR = ROOT / "data" / "extras" / "flood_shp"
flood_max = gpd.read_file(FLOOD_DIR / "shinsui_souteisaidai" / "shinsui_souteisaidai.shp").to_crs(TARGET_CRS)
flood_plan = gpd.read_file(FLOOD_DIR / "shinsui_keikaku" / "shinsui_keikakukibo.shp").to_crs(TARGET_CRS)
# 3D → 2D 化
flood_max["geometry"] = gpd.GeoSeries(shapely.force_2d(flood_max.geometry.values), crs=TARGET_CRS)
flood_plan["geometry"] = gpd.GeoSeries(shapely.force_2d(flood_plan.geometry.values), crs=TARGET_CRS)


def fix_invalid(gdf, label=""):
    """無効ジオメトリだけ buffer(0) で修復 (要件S 対策: 全件バッファより数倍速い)"""
    geoms = gdf.geometry.values.copy()
    inv = ~shapely.is_valid(geoms)
    n_inv = int(inv.sum())
    if n_inv == 0:
        return gdf
    t = time.time()
    for i in range(len(geoms)):
        if inv[i]:
            try:
                geoms[i] = geoms[i].buffer(0)
            except Exception:
                geoms[i] = geoms[i]
    gdf = gdf.copy()
    gdf["geometry"] = geoms
    print(f"  fix_invalid({label}): {n_inv}/{len(gdf)} 件を修復, {time.time()-t:.1f}s")
    return gdf

flood_max = fix_invalid(flood_max, "flood_max")
flood_plan = fix_invalid(flood_plan, "flood_plan")
flood_max["area_m2"] = flood_max.geometry.area
flood_plan["area_m2"] = flood_plan.geometry.area
print(f"  想定最大規模: {len(flood_max)} polys, 計画規模: {len(flood_plan)} polys, "
      f"{time.time()-t1:.1f}s")

# === 2. 津波 dissolve キャッシュ ==========================================
print("\n[2] 津波 浸水深 dissolve キャッシュ")
TSUNAMI_SHP = (ROOT / "data" / "extras" / "tsunami_extracted"
               / "340006_tsunami_inundation_assumption_map_20251203" / "浸水メッシュ.shp")
TSUNAMI_CACHE = ASSETS / "L08_tsunami_dissolved.gpkg"

# 浸水深→rank マッピング (河川 rank と同じ 8 段階)
RANK_BINS = [0, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0, 20.0, 999]
RANK_CODES = [10, 20, 30, 40, 50, 60, 70, 80]

if not TSUNAMI_CACHE.exists():
    print(f"  キャッシュ未作成 → ビルドします (~50 秒, 1.25M メッシュ → 8 polygon)")
    import pyogrio
    tcb = time.time()
    df = pyogrio.read_dataframe(TSUNAMI_SHP, read_geometry=False)
    df.columns = ["x", "y", "depth"]
    df["rank"] = pd.cut(df["depth"], bins=RANK_BINS, labels=RANK_CODES, right=False).astype(int)
    out_geoms, out_ranks = [], []
    for rk in RANK_CODES:
        sub = df[df["rank"] == rk]
        if len(sub) == 0:
            continue
        # 各 (x, y) は 10m メッシュ中心、polygon は ±5m の正方形
        polys = shapely.box(
            sub["x"].values - 5, sub["y"].values - 5,
            sub["x"].values + 5, sub["y"].values + 5,
        )
        merged = shapely.unary_union(polys)
        out_geoms.append(merged)
        out_ranks.append(rk)
    custom_crs = ("+proj=tmerc +lat_0=36 +lon_0=132.166666 +k=0.9999 "
                  "+x_0=0 +y_0=0 +ellps=WGS84 +units=m +no_defs")
    gdf = gpd.GeoDataFrame({"rank": out_ranks, "geometry": out_geoms}, crs=custom_crs)
    gdf.to_crs(TARGET_CRS).to_file(TSUNAMI_CACHE, driver="GPKG")
    print(f"  ビルド完了: {time.time()-tcb:.1f}s, "
          f"{TSUNAMI_CACHE.stat().st_size/1e6:.1f} MB")

t2 = time.time()
tsunami = gpd.read_file(TSUNAMI_CACHE).to_crs(TARGET_CRS)
tsunami = fix_invalid(tsunami, "tsunami")
tsunami["depth_label"] = tsunami["rank"].map(DEPTH_LABEL)
tsunami["area_m2"] = tsunami.geometry.area
print(f"  キャッシュ読み込み: {len(tsunami)} polys, {time.time()-t2:.1f}s")
print(f"  rank 別面積 (km²):")
for _, r in tsunami.iterrows():
    print(f"    rank {r['rank']:>2} ({DEPTH_LABEL[r['rank']]}): {r['area_m2']/1e6:.1f}")

# === 3. 用途地域 GeoJSON (340006 = 県全域) ================================
print("\n[3] 用途地域 GeoJSON (県全域)")
t3 = time.time()
LANDUSE_ZIP = ROOT / "data" / "extras" / "landuse_zone.zip"
LANDUSE_EXTRACT = ROOT / "data" / "extras" / "landuse_extracted"
target_geojson = LANDUSE_EXTRACT / "340006_city_planning_area_various_use_geojson_20220324.geojson"
if not target_geojson.exists() and LANDUSE_ZIP.exists():
    LANDUSE_EXTRACT.mkdir(parents=True, exist_ok=True)
    with zipfile.ZipFile(LANDUSE_ZIP) as zf:
        zf.extractall(LANDUSE_EXTRACT)

landuse = gpd.read_file(target_geojson)
if landuse.crs is None:
    landuse = landuse.set_crs("EPSG:4326")
landuse = landuse.to_crs(TARGET_CRS)
landuse = fix_invalid(landuse, "landuse")
print(f"  features: {len(landuse)}, {time.time()-t3:.1f}s")

YOTO_NAME = {
    1: "第一種低層住居専用", 2: "第二種低層住居専用",
    3: "第一種中高層住居専用", 4: "第二種中高層住居専用",
    5: "第一種住居", 6: "第二種住居", 7: "準住居",
    8: "近隣商業", 9: "商業", 10: "準工業", 11: "工業", 12: "工業専用",
    13: "田園住居",
}
landuse["yoto_name"] = landuse["YOTO_CD"].map(lambda v: YOTO_NAME.get(int(v), f"用途{v}"))
# CITY_CD → 市町名 (広島県 都道府県コード末尾)
CITY_CD_NAME = {
    101: "広島市中区", 102: "広島市東区", 103: "広島市南区", 104: "広島市西区",
    105: "広島市安佐南区", 106: "広島市安佐北区", 107: "広島市安芸区", 108: "広島市佐伯区",
    202: "呉市", 203: "竹原市", 204: "三原市", 205: "尾道市", 207: "福山市",
    208: "府中市", 209: "三次市", 210: "庄原市", 211: "大竹市", 212: "東広島市",
    213: "廿日市市", 214: "安芸高田市", 215: "江田島市",
    302: "府中町", 304: "海田町", 307: "熊野町", 309: "坂町",
    369: "安芸太田町", 462: "世羅町",
}
landuse["city_name"] = landuse["CITY_CD"].map(CITY_CD_NAME).fillna("(その他)")

# 用途別 dissolve
td_d = time.time()
landuse_d = landuse[["yoto_name", "geometry"]].dissolve(by="yoto_name").reset_index()
landuse_d["total_ha"] = landuse_d.geometry.area / 10000
print(f"  用途別 dissolve: {len(landuse_d)} 用途, {time.time()-td_d:.1f}s")

# 市町別 dissolve (盛土点を市町に紐付ける用)
tc_d = time.time()
city_d = landuse[["city_name", "geometry"]].dissolve(by="city_name").reset_index()
city_d["total_ha"] = city_d.geometry.area / 10000
print(f"  市町別 dissolve: {len(city_d)} 市町, {time.time()-tc_d:.1f}s")

# === 4. 大規模盛土造成地 — xlsx → 受付窓口の市町を抽出 ====================
# xlsx は受付窓口の市町リスト (位置座標は無い)。教材としての現実的工夫:
# 各受付対象市町について、該当用途地域 polygon の内部にランダム代表点を生成
# (シード固定で再現性確保)。本データは「規制対象とされた市町に大規模盛土が
# 一定数存在する」という"分布の概形"を示す illustrative sample である旨を
# HTML 内で明示する。
print("\n[4] 大規模盛土造成地 サンプル点 (xlsx 市町リストから生成)")
t4 = time.time()
EARTH_XLSX = ROOT / "data" / "extras" / "earth_fill_authorized.xlsx"
df_xls = pd.read_excel(EARTH_XLSX, sheet_name=1, header=5)
df_xls.columns = ["区分", "機関1", "部署1", "tel1", "機関2", "部署2", "tel2",
                  "機関3", "部署3", "tel3", "機関4", "部署4", "tel4"]
ef_munis = df_xls["区分"].dropna().astype(str).str.strip().tolist()
ef_munis = [m for m in ef_munis if m and m != "nan"]
print(f"  受付対象 市町: {len(ef_munis)} 件 → {ef_munis[:5]}…")

# 各市町に対して 8 件の代表点を polygon 内部に生成
rng = np.random.default_rng(42)
ef_pts = []
for muni in ef_munis:
    matches = city_d[city_d["city_name"].str.startswith(muni[:2])]  # 簡易一致
    if matches.empty:
        # 「広島市」全体は分割されているので個別区との一致を試す
        matches = city_d[city_d["city_name"].str.contains(muni, regex=False)]
    if matches.empty:
        continue
    poly = shapely.union_all(matches.geometry.values, grid_size=0.01)
    if poly.is_empty:
        continue
    minx, miny, maxx, maxy = poly.bounds
    n_target = 8
    n_made = 0
    tries = 0
    while n_made < n_target and tries < 200:
        x = rng.uniform(minx, maxx)
        y = rng.uniform(miny, maxy)
        p = shapely.Point(x, y)
        if poly.contains(p):
            ef_pts.append({"muni": muni, "x": x, "y": y, "geometry": p})
            n_made += 1
        tries += 1

earth_fill = gpd.GeoDataFrame(ef_pts, geometry="geometry", crs=TARGET_CRS)
print(f"  生成サンプル点: {len(earth_fill)} 点 ({time.time()-t4:.1f}s)")

# === 5. 描画範囲とラスタライズ前処理 ========================================
# 飛び抜けて重い vector 描画を避けるため、河川/津波/用途を 25m grid に
# rasterize し、imshow で描画する。後段の集計でも使い回す。
print("\n[5] 描画範囲設定 + 全レイヤのラスタライズ前処理")
t5 = time.time()
tsu_box = tsunami.total_bounds  # [minx miny maxx maxy]
mx0, my0, mx1, my1 = tsu_box
PAD = 5000
import rasterio
from rasterio.features import rasterize

PIXEL = 50  # 50m grid (描画とメモリのバランス。25m → 50m で 4 倍軽量)
mx0g, my0g, mx1g, my1g = mx0 - PAD, my0 - PAD, mx1 + PAD, my1 + PAD
W = int((mx1g - mx0g) / PIXEL) + 1
H = int((my1g - my0g) / PIXEL) + 1
transform = rasterio.transform.from_origin(mx0g, my1g, PIXEL, PIXEL)
extent = (mx0g, mx1g, my0g, my1g)  # imshow extent (left, right, bottom, top)
print(f"  grid: {W} × {H} = {W*H/1e6:.1f}M cells")

t = time.time()
fm_rank_rast = rasterize(
    [(g, int(r)) for g, r in zip(flood_max.geometry, flood_max["rank"])
     if g is not None and not g.is_empty],
    out_shape=(H, W), transform=transform, fill=0, dtype="uint8",
)
print(f"  rasterize flood_max: {time.time()-t:.1f}s")
t = time.time()
fp_rank_rast = rasterize(
    [(g, int(r)) for g, r in zip(flood_plan.geometry, flood_plan["rank"])
     if g is not None and not g.is_empty],
    out_shape=(H, W), transform=transform, fill=0, dtype="uint8",
)
print(f"  rasterize flood_plan: {time.time()-t:.1f}s")
t = time.time()
ts_rank_rast = rasterize(
    [(g, int(r)) for g, r in zip(tsunami.geometry, tsunami["rank"])
     if g is not None and not g.is_empty],
    out_shape=(H, W), transform=transform, fill=0, dtype="uint8",
)
print(f"  rasterize tsunami: {time.time()-t:.1f}s")
t = time.time()
landuse_id_rast = rasterize(
    [(g, i + 1) for i, (_, row) in enumerate(landuse_d.iterrows())
     for g in [row.geometry] if g is not None and not g.is_empty],
    out_shape=(H, W), transform=transform, fill=0, dtype="uint8",
)
print(f"  rasterize landuse: {time.time()-t:.1f}s")

PIX_HA = (PIXEL ** 2) / 10000  # 1 pixel = 0.0625 ha
# 共通 bool マスク
fm_b = fm_rank_rast > 0
fp_b = fp_rank_rast > 0
ts_b = ts_rank_rast > 0
landuse_bg = (landuse_id_rast > 0)
solo_max_ha = float(fm_b.sum() * PIX_HA)
solo_plan_ha = float(fp_b.sum() * PIX_HA)
solo_tsu_ha = float(ts_b.sum() * PIX_HA)
double_b = fm_b & ts_b
triple_b = fm_b & ts_b & fp_b
double_ha = float(double_b.sum() * PIX_HA)
triple_ha = float(triple_b.sum() * PIX_HA)
print(f"  単独面積: 河川最大 {solo_max_ha:.0f} ha / 計画 {solo_plan_ha:.0f} ha / "
      f"津波 {solo_tsu_ha:.0f} ha")
print(f"  ダブル: {double_ha:.0f} ha / トリプル: {triple_ha:.0f} ha")
print(f"  total {time.time()-t5:.1f}s")

# === 6. 図1: 3 重浸水主題図 (imshow ラスタ重ね合わせ) =====================
print("\n[6] 図1 三重浸水主題図 (重ね合わせ, imshow ラスタ)")
tf1 = time.time()
import matplotlib.colors as mcolors

def rast_to_rgba(rast_bool, color, alpha=0.6):
    """Bool ラスタを RGBA uint8 画像に変換 (True セルだけ着色, メモリ効率重視)"""
    h, w = rast_bool.shape
    rgba = np.zeros((h, w, 4), dtype=np.uint8)
    rgb = mcolors.to_rgb(color)
    rgba[rast_bool, 0] = int(rgb[0] * 255)
    rgba[rast_bool, 1] = int(rgb[1] * 255)
    rgba[rast_bool, 2] = int(rgb[2] * 255)
    rgba[rast_bool, 3] = int(alpha * 255)
    return rgba

fig, ax = plt.subplots(figsize=(13, 9))
# 背景: 用途地域あり領域を薄グレー
landuse_bg = (landuse_id_rast > 0)
ax.imshow(rast_to_rgba(landuse_bg, "#dddddd", 0.5), extent=extent, origin="upper")
# 河川計画 (薄青)
ax.imshow(rast_to_rgba(fp_rank_rast > 0, "#56a4dc", 0.55), extent=extent, origin="upper")
# 河川最大 (濃青)
ax.imshow(rast_to_rgba(fm_rank_rast > 0, "#0e4282", 0.5), extent=extent, origin="upper")
# 津波 (橙〜赤、深さで濃淡)
tsu_color_per_rank = {10: "#ffd9a8", 20: "#ffaa66", 30: "#ff7733",
                      40: "#e74c3c", 50: "#a4051a", 60: "#5a0210"}
for rk in [10, 20, 30, 40, 50, 60]:
    mask = (ts_rank_rast == rk)
    if mask.any():
        ax.imshow(rast_to_rgba(mask, tsu_color_per_rank[rk], 0.6),
                  extent=extent, origin="upper")
# 盛土点 (vector のままで OK、点なので速い)
earth_fill.plot(ax=ax, color="black", markersize=22,
                edgecolor="white", linewidth=0.7, zorder=10)

ax.set_xlim(mx0 - PAD, mx1 + PAD)
ax.set_ylim(my0 - PAD, my1 + PAD)
ax.set_aspect("equal")
ax.set_xlabel("X (m, JGD2011 Plane Rectangular III)")
ax.set_ylabel("Y (m)")
ax.set_title("広島県沿岸 — 河川計画規模 (薄青) × 河川想定最大規模 (濃青) "
             "× 津波 (橙〜赤) × 大規模盛土サンプル (黒点)\n"
             "重なる場所が複合災害高リスク領域", fontsize=12)
legend_handles = [
    Patch(facecolor="#56a4dc", alpha=0.55, label="河川 計画規模"),
    Patch(facecolor="#0e4282", alpha=0.5, label="河川 想定最大規模"),
    Patch(facecolor="#ff7733", alpha=0.6, label="津波 rank 30 (1-2m)"),
    Patch(facecolor="#e74c3c", alpha=0.6, label="津波 rank 40 (2-3m)"),
    Patch(facecolor="#a4051a", alpha=0.6, label="津波 rank 50 (3-5m)"),
    Patch(facecolor="black", label="大規模盛土サンプル点"),
]
ax.legend(handles=legend_handles, loc="upper right", fontsize=9, framealpha=0.95)
plt.tight_layout()
plt.savefig(ASSETS / "L08_multi_overlay_main.png", dpi=120)
plt.close()
print(f"  完了: {time.time()-tf1:.1f}s")

# === 7. 図2: 3 つの個別主題図 (small multiples, imshow ラスタ) =============
print("\n[7] 図2 個別主題図 small multiples")
tf2 = time.time()

def draw_rank_panel(ax, rank_rast, title):
    ax.imshow(rast_to_rgba(landuse_bg, "#dddddd", 0.4),
              extent=extent, origin="upper")
    for rk in [10, 20, 30, 40, 50, 60, 70, 80]:
        mask = (rank_rast == rk)
        if mask.any():
            ax.imshow(rast_to_rgba(mask, DEPTH_COLOR[rk], 0.85),
                      extent=extent, origin="upper")
    ax.set_xlim(mx0 - PAD, mx1 + PAD)
    ax.set_ylim(my0 - PAD, my1 + PAD)
    ax.set_aspect("equal")
    ax.set_title(title, fontsize=11)
    ax.set_xticks([]); ax.set_yticks([])

fig, axes = plt.subplots(1, 3, figsize=(18, 7))
draw_rank_panel(axes[0], fp_rank_rast,
                f"(a) 河川 計画規模  {solo_plan_ha:.0f} ha")
draw_rank_panel(axes[1], fm_rank_rast,
                f"(b) 河川 想定最大規模  {solo_max_ha:.0f} ha")
draw_rank_panel(axes[2], ts_rank_rast,
                f"(c) 津波 想定最大規模  {solo_tsu_ha:.0f} ha")

depth_legend = [Patch(facecolor=DEPTH_COLOR[r], label=DEPTH_LABEL[r])
                for r in [10, 20, 30, 40, 50, 60]]
fig.legend(handles=depth_legend, loc="lower center", ncol=6,
           bbox_to_anchor=(0.5, -0.02), fontsize=9, title="浸水深ランク (共通)")
plt.suptitle("3 災害の浸水想定 個別主題図 — 同一スケール、同一深さ凡例で比較",
             fontsize=13)
plt.tight_layout()
plt.savefig(ASSETS / "L08_three_layers_small_multiples.png",
            dpi=120, bbox_inches="tight")
plt.close()
print(f"  完了: {time.time()-tf2:.1f}s")

# === 8. ダブル/トリプルリスク領域 (前計算結果を再利用) =====================
# Step 5 で raster 集計済み。ここでは描画用に double 領域を polygon 化
print("\n[8] ダブル/トリプルリスク領域 polygon 化")
t8 = time.time()
from rasterio.features import shapes as rio_shapes
double_polys = []
for shape, val in rio_shapes(double_b.astype("uint8"), mask=double_b, transform=transform):
    double_polys.append(shapely.geometry.shape(shape))
double = gpd.GeoDataFrame(geometry=double_polys, crs=TARGET_CRS)
print(f"  double polygon: {len(double)}, {time.time()-t8:.1f}s")

# === 9. 図3: ダブルリスク領域マップ (imshow ラスタ) =======================
print("\n[9] 図3 ダブルリスク領域マップ")
tf3 = time.time()
fig, ax = plt.subplots(figsize=(13, 9))
ax.imshow(rast_to_rgba(landuse_bg, "#eeeeee", 0.5),
          extent=extent, origin="upper")
ax.imshow(rast_to_rgba(fm_b & ~ts_b, "#56a4dc", 0.4),
          extent=extent, origin="upper")
ax.imshow(rast_to_rgba(ts_b & ~fm_b, "#ffaa66", 0.4),
          extent=extent, origin="upper")
# ダブルリスク (赤)
ax.imshow(rast_to_rgba(double_b, "#cf222e", 0.95),
          extent=extent, origin="upper")
earth_fill.plot(ax=ax, color="black", markersize=20,
                edgecolor="white", linewidth=0.6, zorder=10)

ax.set_xlim(mx0 - PAD, mx1 + PAD)
ax.set_ylim(my0 - PAD, my1 + PAD)
ax.set_aspect("equal")
ax.set_xlabel("X (m)"); ax.set_ylabel("Y (m)")
ax.set_title(f"ダブル浸水リスク領域 (赤): 河川想定最大 ∩ 津波 = {double_ha:.0f} ha\n"
             f"(青 = 河川単独 / 橙 = 津波単独 / 黒点 = 大規模盛土サンプル)",
             fontsize=12)
legend_handles = [
    Patch(facecolor="#56a4dc", alpha=0.4, label="河川単独"),
    Patch(facecolor="#ffaa66", alpha=0.4, label="津波単独"),
    Patch(facecolor="#cf222e", alpha=0.95, label="ダブルリスク (河川 ∩ 津波)"),
    Patch(facecolor="black", label="大規模盛土サンプル"),
]
ax.legend(handles=legend_handles, loc="upper right", fontsize=9, framealpha=0.95)
plt.tight_layout()
plt.savefig(ASSETS / "L08_double_risk_map.png", dpi=120)
plt.close()
print(f"  完了: {time.time()-tf3:.1f}s")

# === 10. ダブルリスク × 市町別分布 (ラスタ集計) ============================
print("\n[10] ダブルリスク × 市町別 (raster 集計)")
t10 = time.time()
# 市町を id でラスタ化 (1〜N の整数)
city_ids = list(range(1, len(city_d) + 1))
city_rast = rasterize(
    [(g, i) for g, i in zip(city_d.geometry, city_ids)
     if g is not None and not g.is_empty],
    out_shape=(H, W), transform=transform, all_touched=False,
    fill=0, dtype="uint16",
)
# 各市町の double セル数を bincount で一括集計
double_per_city = np.bincount(
    city_rast[double_b].ravel(),
    minlength=len(city_ids) + 1,
)
city_double = []
for i, (_, row) in enumerate(city_d.iterrows()):
    n_double = int(double_per_city[i + 1])
    city_double.append({"city": row["city_name"],
                        "double_ha": n_double * PIX_HA,
                        "city_total_ha": row["total_ha"]})
city_double_df = (pd.DataFrame(city_double)
                  .sort_values("double_ha", ascending=False))
city_double_df["double_pct"] = (city_double_df["double_ha"]
                                / city_double_df["city_total_ha"] * 100).round(2)
city_double_df.to_csv(ASSETS / "L08_double_city.csv",
                      index=False, encoding="utf-8-sig")
print(city_double_df.head(10).to_string())

# 図4: 市町別ダブルリスク棒グラフ (上位 12)
fig, ax = plt.subplots(figsize=(11, 6))
top = city_double_df[city_double_df["double_ha"] > 0].head(12)
ax.barh(top["city"][::-1], top["double_ha"][::-1],
        color="#cf222e", edgecolor="black", linewidth=0.4)
for i, (_, r) in enumerate(top[::-1].iterrows()):
    ax.text(r["double_ha"] + 1, i, f"{r['double_ha']:.0f} ha "
            f"({r['double_pct']:.1f}%)",
            va="center", fontsize=9)
ax.set_xlabel("ダブルリスク領域面積 (ha) — 河川想定最大 ∩ 津波")
ax.set_title("市町別 ダブル浸水リスク領域 (上位 12)\n"
             "数値の括弧内: 市町用途地域に対する割合", fontsize=11)
ax.grid(axis="x", alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "L08_double_city_bar.png", dpi=130)
plt.close()
print(f"  完了: {time.time()-t10:.1f}s")

# === 11. 用途別 ダブルリスク領域 (ラスタ集計) ==============================
print("\n[11] 用途別 ダブルリスク領域 (raster 集計)")
t11 = time.time()
# 用途を id でラスタ化
yoto_ids = list(range(1, len(landuse_d) + 1))
yoto_rast = rasterize(
    [(g, i) for g, i in zip(landuse_d.geometry, yoto_ids)
     if g is not None and not g.is_empty],
    out_shape=(H, W), transform=transform, all_touched=False,
    fill=0, dtype="uint16",
)
double_per_yoto = np.bincount(
    yoto_rast[double_b].ravel(),
    minlength=len(yoto_ids) + 1,
)
yoto_summary_rows = []
for i, (_, row) in enumerate(landuse_d.iterrows()):
    n_double = int(double_per_yoto[i + 1])
    yoto_summary_rows.append({
        "yoto_name": row["yoto_name"],
        "overlap_ha": n_double * PIX_HA,
        "total_ha": row["total_ha"],
    })
yoto_summary = pd.DataFrame(yoto_summary_rows)
yoto_summary["double_pct"] = (yoto_summary["overlap_ha"]
                              / yoto_summary["total_ha"] * 100).round(2)
yoto_summary = yoto_summary.sort_values("overlap_ha", ascending=False)
yoto_summary.to_csv(ASSETS / "L08_double_yoto.csv",
                    index=False, encoding="utf-8-sig")
print(yoto_summary.head(10).to_string())

# 図5: 用途別ダブルリスク
fig, ax = plt.subplots(figsize=(11, 6))
top = yoto_summary[yoto_summary["overlap_ha"] > 0].head(13)
ax.barh(top["yoto_name"][::-1], top["overlap_ha"][::-1],
        color="#7d2cbf", edgecolor="black", linewidth=0.4)
for i, (_, r) in enumerate(top[::-1].iterrows()):
    ax.text(r["overlap_ha"] + 0.5, i, f"{r['overlap_ha']:.0f} ha "
            f"({r['double_pct']:.1f}%)",
            va="center", fontsize=9)
ax.set_xlabel("ダブルリスク領域面積 (ha)")
ax.set_title("用途地域 × ダブル浸水リスク (河川想定最大 ∩ 津波)\n"
             "括弧内: 各用途総面積に対するダブルリスク割合", fontsize=11)
ax.grid(axis="x", alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "L08_double_yoto_bar.png", dpi=130)
plt.close()
print(f"  完了: {time.time()-t11:.1f}s")

# === 12. 盛土サンプル点 × 各浸水域 sjoin =====================================
print("\n[12] 盛土サンプル × 浸水域 sjoin")
t12 = time.time()
# 河川最大規模に含まれるか
ef_in_max = gpd.sjoin(earth_fill, flood_max[["rank", "geometry"]],
                       how="left", predicate="within")
ef_in_max = ef_in_max.rename(columns={"rank": "rank_river_max"})
ef_in_max = ef_in_max.drop(columns=["index_right"], errors="ignore")
ef_in_plan = gpd.sjoin(ef_in_max, flood_plan[["rank", "geometry"]],
                        how="left", predicate="within")
ef_in_plan = ef_in_plan.rename(columns={"rank": "rank_river_plan"})
ef_in_plan = ef_in_plan.drop(columns=["index_right"], errors="ignore")
ef_in_tsu = gpd.sjoin(ef_in_plan, tsunami[["rank", "geometry"]],
                       how="left", predicate="within")
ef_in_tsu = ef_in_tsu.rename(columns={"rank": "rank_tsunami"})
ef_in_tsu = ef_in_tsu.drop(columns=["index_right"], errors="ignore")

# 重複除去 (同一点が複数 rank に sjoin される場合があるので最大 rank を採用)
def reduce_max(df, key, col):
    g = (df.groupby(key)[col].max().reset_index())
    return g

ef_pts_df = pd.DataFrame({
    "muni": earth_fill["muni"].values,
    "x": earth_fill["x"].values,
    "y": earth_fill["y"].values,
})
ef_pts_df["pt_id"] = range(len(ef_pts_df))
# ef_in_tsu には pt_id が無いので、index 経由でマージ
def per_pt_max(df, col):
    return (df.assign(pt_id=df.index)
            .groupby("pt_id")[col].max().reset_index())

# 上記 sjoin は inner=left、複数行は同 pt_id で並ぶ — index 一致を使う
ef_in_max_m = (ef_in_max[["rank_river_max"]].reset_index()
               .groupby("index")["rank_river_max"].max().reset_index()
               .rename(columns={"index": "pt_id"}))
# シンプル化: 各点について 3 災害 rank を埋める
def lookup_rank(point, gdf, col="rank"):
    sub = gdf[gdf.intersects(point)]
    if sub.empty:
        return np.nan
    return int(sub[col].max())

ef_pts_df["rank_river_max"] = [lookup_rank(p, flood_max) for p in earth_fill.geometry]
ef_pts_df["rank_river_plan"] = [lookup_rank(p, flood_plan) for p in earth_fill.geometry]
ef_pts_df["rank_tsunami"] = [lookup_rank(p, tsunami) for p in earth_fill.geometry]
ef_pts_df["risk_count"] = (
    ef_pts_df["rank_river_max"].notna().astype(int)
    + ef_pts_df["rank_tsunami"].notna().astype(int)
)
ef_pts_df.to_csv(ASSETS / "L08_earthfill_sjoin.csv",
                 index=False, encoding="utf-8-sig")

n_in_max = ef_pts_df["rank_river_max"].notna().sum()
n_in_plan = ef_pts_df["rank_river_plan"].notna().sum()
n_in_tsu = ef_pts_df["rank_tsunami"].notna().sum()
n_in_double = ((ef_pts_df["rank_river_max"].notna())
               & (ef_pts_df["rank_tsunami"].notna())).sum()
print(f"  サンプル盛土 {len(ef_pts_df)} 点中:")
print(f"    河川計画規模に入る: {n_in_plan}")
print(f"    河川想定最大に入る: {n_in_max}")
print(f"    津波想定に入る:     {n_in_tsu}")
print(f"    ダブル (両方):     {n_in_double}")
print(f"  処理: {time.time()-t12:.1f}s")

# === 13. 図4: 浸水深ランク別 面積比較 (河川 vs 津波, ラスタ集計) ============
print("\n[13] 図4 浸水深ランク別面積比較 (raster 集計)")
tf4 = time.time()
rank_compare = []
for rk in RANK_CODES:
    # 各ラスタで該当 rank のセル数を数える (重複ポリゴン排除)
    rmx = float((fm_rank_rast == rk).sum() * PIX_HA)
    rpl = float((fp_rank_rast == rk).sum() * PIX_HA)
    tsa = float((ts_rank_rast == rk).sum() * PIX_HA)
    rank_compare.append({"rank": rk, "label": DEPTH_LABEL[rk],
                         "river_max_ha": rmx, "river_plan_ha": rpl,
                         "tsunami_ha": tsa})
rank_cmp_df = pd.DataFrame(rank_compare)
rank_cmp_df.to_csv(ASSETS / "L08_rank_compare.csv",
                   index=False, encoding="utf-8-sig")

fig, ax = plt.subplots(figsize=(11, 5.6))
x = np.arange(len(rank_cmp_df))
w = 0.27
ax.bar(x - w, rank_cmp_df["river_plan_ha"], w, label="河川 計画規模",
       color="#56a4dc", edgecolor="black", linewidth=0.4)
ax.bar(x,     rank_cmp_df["river_max_ha"], w, label="河川 想定最大",
       color="#0e4282", edgecolor="black", linewidth=0.4)
ax.bar(x + w, rank_cmp_df["tsunami_ha"], w, label="津波 想定最大",
       color="#cf222e", edgecolor="black", linewidth=0.4)
ax.set_xticks(x)
ax.set_xticklabels(rank_cmp_df["label"], rotation=20, ha="right")
ax.set_ylabel("面積 (ha)")
ax.set_yscale("log")
ax.set_title("浸水深ランク別 面積 (河川計画 vs 河川最大 vs 津波) — 対数スケール\n"
             "河川は深い帯まで分布、津波は浅瀬中心の傾向", fontsize=11)
ax.legend()
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "L08_rank_compare_bar.png", dpi=130)
plt.close()
print(f"  完了: {time.time()-tf4:.1f}s")

# === 14. 図5: 盛土点 × 危険度散布 (ランク × ランク) ========================
print("\n[14] 図5 盛土点 × 危険度散布")
tf5 = time.time()
fig, ax = plt.subplots(figsize=(8.5, 7.5))
sub = ef_pts_df.dropna(subset=["rank_river_max", "rank_tsunami"])
ax.scatter(sub["rank_river_max"], sub["rank_tsunami"],
           s=80, alpha=0.65, color="#cf222e", edgecolor="black", linewidth=0.5)
# 単独 (片方のみ)
solo_river = ef_pts_df[ef_pts_df["rank_river_max"].notna()
                       & ef_pts_df["rank_tsunami"].isna()]
solo_tsu = ef_pts_df[ef_pts_df["rank_tsunami"].notna()
                     & ef_pts_df["rank_river_max"].isna()]
ax.scatter(solo_river["rank_river_max"], np.zeros(len(solo_river)) - 5,
           s=80, alpha=0.65, color="#0e4282", edgecolor="black",
           linewidth=0.5, label="河川のみ (y軸 -5 に散布)")
ax.scatter(np.zeros(len(solo_tsu)) - 5, solo_tsu["rank_tsunami"],
           s=80, alpha=0.65, color="#fb8500", edgecolor="black",
           linewidth=0.5, label="津波のみ (x軸 -5 に散布)")
ax.set_xlabel("河川 想定最大規模 rank (10=0.5m未満 〜 80=20m以上)")
ax.set_ylabel("津波 想定最大 rank (同上)")
ax.set_xlim(-15, 90); ax.set_ylim(-15, 90)
ax.axhline(0, color="#999", lw=0.5); ax.axvline(0, color="#999", lw=0.5)
ax.set_title(f"大規模盛土サンプル {len(ef_pts_df)} 点 × 浸水深 rank 散布\n"
             f"赤 = ダブル ({len(sub)} 点) / 青/橙 = 単独", fontsize=11)
ax.legend(loc="upper right", fontsize=9)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "L08_earthfill_risk_scatter.png", dpi=130)
plt.close()
print(f"  完了: {time.time()-tf5:.1f}s")

# === 15. ダブルリスク × rank ピボット ======================================
print("\n[15] ダブルリスクの河川 rank × 津波 rank ピボット")
t15 = time.time()
# rank ごとに intersection 面積を計算 (重い処理: 8x8 = 64 ペア)
# 高速化: rank ごとに dissolve 済 (tsunami は既に dissolve、river は rank ごとに dissolve)
# raster 集計: rank × rank の同一セル数 → bool 演算で一発
pivot_data = np.zeros((8, 8))
for i, r_rk in enumerate(RANK_CODES):
    fm_mask = (fm_rank_rast == r_rk)
    if not fm_mask.any(): continue
    for j, t_rk in enumerate(RANK_CODES):
        ts_mask = (ts_rank_rast == t_rk)
        if not ts_mask.any(): continue
        pivot_data[i, j] = float((fm_mask & ts_mask).sum() * PIX_HA)
pivot_df = pd.DataFrame(pivot_data,
                        index=[DEPTH_LABEL[r] for r in RANK_CODES],
                        columns=[DEPTH_LABEL[r] for r in RANK_CODES])
pivot_df.to_csv(ASSETS / "L08_double_rank_pivot.csv", encoding="utf-8-sig")

# 図6: ダブルリスク pivot ヒートマップ
fig, ax = plt.subplots(figsize=(10, 6.5))
mask = pivot_data > 0
display = np.where(mask, pivot_data, np.nan)
im = ax.imshow(display, cmap="YlOrRd", aspect="auto")
ax.set_xticks(range(8))
ax.set_xticklabels([DEPTH_LABEL[r] for r in RANK_CODES], rotation=25, ha="right")
ax.set_yticks(range(8))
ax.set_yticklabels([DEPTH_LABEL[r] for r in RANK_CODES])
plt.colorbar(im, ax=ax, label="ダブルリスク面積 (ha)")
ax.set_xlabel("津波 浸水深ランク")
ax.set_ylabel("河川 想定最大 浸水深ランク")
for i in range(8):
    for j in range(8):
        v = pivot_data[i, j]
        if v >= 0.1:
            ax.text(j, i, f"{v:.0f}", ha="center", va="center",
                    fontsize=8,
                    color="white" if v > pivot_data.max() * 0.55 else "black")
ax.set_title("ダブルリスク領域: 河川 rank × 津波 rank の重なり面積 (ha)\n"
             "数字は重なり面積 (ha)。空白は重なり 0", fontsize=11)
plt.tight_layout()
plt.savefig(ASSETS / "L08_double_rank_pivot.png", dpi=130)
plt.close()
print(f"  完了: {time.time()-t15:.1f}s")

# === 16. 中間 CSV: 用途×市町×重ね合わせサマリ =============================
print("\n[16] サマリテーブル CSV 出力")
yoto_summary.drop(columns=[c for c in ("geometry",) if c in yoto_summary.columns]).to_csv(
    ASSETS / "L08_double_yoto.csv", index=False, encoding="utf-8-sig")

# 全体サマリ
overall = pd.DataFrame({
    "scope": ["河川計画規模", "河川想定最大", "津波想定最大",
              "ダブル (河川最大∩津波)", "トリプル (3 全部)"],
    "面積_ha": [solo_plan_ha, solo_max_ha, solo_tsu_ha, double_ha, triple_ha],
    "面積_km2": [solo_plan_ha/100, solo_max_ha/100, solo_tsu_ha/100,
                 double_ha/100, triple_ha/100],
})
overall.to_csv(ASSETS / "L08_overall.csv", index=False, encoding="utf-8-sig")
print(overall.to_string())

# === 17. 仮説判定 =========================================================
print("\n[17] 仮説判定")
H1 = "支持" if double_ha >= 100 else "反証"
top_double_city = (city_double_df[city_double_df["double_ha"] > 0].iloc[0]["city"]
                    if (city_double_df["double_ha"] > 0).any() else "-")
H2 = "部分支持" if "広島市" in top_double_city or any(
    c in top_double_city for c in ["南区", "西区", "中区"]) else "再検討"
H3 = "支持" if n_in_double >= 1 else "反証"
# raster ベースの正確な深部面積
H4_river_deep = float(((fm_rank_rast >= 50) & (fm_rank_rast > 0)).sum() * PIX_HA / 100)  # km²
H4_tsu_deep = float(((ts_rank_rast >= 50) & (ts_rank_rast > 0)).sum() * PIX_HA / 100)  # km²
H4 = "支持" if H4_river_deep > H4_tsu_deep else "反証"
top_yoto_double = (yoto_summary.iloc[0]["yoto_name"]
                    if not yoto_summary.empty else "-")
H5 = "部分支持" if any(k in top_yoto_double for k in ["工業", "商業", "準工業"]) else "再検討"

verdict = {
    "H1: ダブル≥100ha": (H1, f"{double_ha:.1f} ha"),
    "H2: 広島市南/西/中区集中": (H2, top_double_city),
    "H3: 盛土の中にダブルリスク有": (H3, f"{n_in_double}/{len(ef_pts_df)} pts"),
    "H4: 河川は深い、津波は浅い": (H4, f"river_deep={H4_river_deep:.1f}km², "
                                         f"tsu_deep={H4_tsu_deep:.2f}km²"),
    "H5: 工業/港湾系に集中": (H5, top_yoto_double),
}
for k, v in verdict.items():
    print(f"  {k}: {v[0]}  ({v[1]})")

elapsed = time.time() - t0
print(f"\n=== 計算フェーズ完了: {elapsed:.1f}s ===")

# ============================================================================
# === HTML レンダリング ======================================================
# ============================================================================
print("\n[HTML レンダリング]")

# テーブル HTML 化
overall_html = overall.to_html(index=False, float_format=lambda x: f"{x:.2f}")
city_top_html = (city_double_df[city_double_df["double_ha"] > 0]
                 .head(12).to_html(index=False, float_format=lambda x: f"{x:.2f}"))
yoto_top_html = (yoto_summary[yoto_summary["overlap_ha"] > 0]
                 .head(13).to_html(index=False, float_format=lambda x: f"{x:.2f}"))
rank_cmp_html = rank_cmp_df.to_html(index=False, float_format=lambda x: f"{x:.1f}")
pivot_html = pivot_df.to_html(float_format=lambda x: f"{x:.1f}" if x > 0.1 else "—")
ef_pts_html = (ef_pts_df.head(15).to_html(index=False))
ef_summary_df = pd.DataFrame({
    "条件": ["河川計画規模に含まれる", "河川想定最大に含まれる",
             "津波想定に含まれる", "ダブル (河川最大 ∩ 津波)"],
    "サンプル点数": [n_in_plan, n_in_max, n_in_tsu, n_in_double],
    "総数比": [f"{n_in_plan/len(ef_pts_df)*100:.1f}%",
                f"{n_in_max/len(ef_pts_df)*100:.1f}%",
                f"{n_in_tsu/len(ef_pts_df)*100:.1f}%",
                f"{n_in_double/len(ef_pts_df)*100:.1f}%"],
})
ef_summary_html = ef_summary_df.to_html(index=False)

verdict_rows = []
for k, (j, ev) in verdict.items():
    verdict_rows.append(f"<tr><td>{k}</td><td><b>{j}</b></td><td>{ev}</td></tr>")
verdict_html = ("<table><tr><th>仮説</th><th>判定</th><th>根拠</th></tr>"
                + "".join(verdict_rows) + "</table>")

sections = [
    ("学習目標と問い", f"""
<div class="note" style="background:#fff5f5; border-left:4px solid #cf222e;">
<b>本レッスンのスタイル: 3 ハザードの GIS 重ね合わせ + 危険度クロス分析</b><br>
広島県沿岸では <b>河川氾濫</b> と <b>津波</b> という独立した災害想定が並存している。
これらが重なる "ダブル浸水リスク" 領域はどこに、どれくらい広がるか?
さらに <b>大規模盛土造成地</b> がそれらの浸水域内にあれば、盛土崩壊+浸水という
二重災害の懸念が生じる。本レッスンでは <code>geopandas.overlay()</code> と
<code>geopandas.sjoin()</code> で 3 レイヤを 1 つのマップに統合し、
<b>複合災害高リスク帯</b> を定量・可視化する。
</div>

<h3>このレッスンで答えたい問い</h3>
<ol>
<li><b>面の問い</b>: ダブル浸水リスク (河川氾濫と津波の両方が想定されている)
領域は何 ha 存在し、どこに分布するか?</li>
<li><b>市町の問い</b>: その領域は広島県のどの市町に集中するか?</li>
<li><b>用途の問い</b>: 用途地域別に見ると、ダブルリスク領域の "中身" はどんな
土地利用か? (住居 / 工業 / 商業)</li>
<li><b>盛土の問い</b>: 大規模盛土造成地サンプルのうち、浸水想定域内 ⊃
ダブルリスク内にあるものはどれくらいか?</li>
<li><b>深さの問い</b>: 河川と津波で浸水深の分布特性は違うか?
(山側で深いか海側で深いか)</li>
</ol>

<h3>立てた仮説 (H1〜H5)</h3>
<ol>
<li><b>H1</b>: ダブルリスク領域 (河川想定最大 ∩ 津波) は <b>≥ 100 ha</b> 存在する
(海に近い低地で河川氾濫と津波が両方届くため)</li>
<li><b>H2</b>: ダブルリスク領域は <b>広島市南区・西区・中区など港湾部</b> に集中
(太田川河口デルタ + 瀬戸内海面)</li>
<li><b>H3</b>: 大規模盛土サンプルの中に、浸水想定域内のものが存在する
(盛土崩壊+浸水の複合リスク)</li>
<li><b>H4</b>: 浸水深の分布は <b>河川の方が深く</b>、津波は <b>浅瀬中心</b>
(河川は山地の谷で 10m+、津波は海岸線から内陸へ減衰)</li>
<li><b>H5</b>: ダブルリスクの用途別構成は <b>工業/港湾施設・商業地</b> に集中
(臨海工業地帯と中心商業地が低地に集まる)</li>
</ol>

<h3>用語の定義 (このレッスン独自)</h3>
<ul>
<li><b>「3 重災害リスク」</b>: 同一地点に <b>河川計画規模・河川想定最大規模・
津波想定最大</b> の 3 つの浸水想定が重なって指定されている領域</li>
<li><b>「ダブルリスク」</b>: <b>河川想定最大 ∩ 津波想定最大</b> の 2 重重ね合わせ
(計画規模を含めて 3 重にすると、計画規模は最大規模の部分集合なので実質ダブル
の評価が中心となる)</li>
<li><b>「rank」</b>: 浸水深の 8 段階コード (10=0.5m未満 〜 80=20m以上)。
河川 Shapefile の <code>rank</code> 列にも、津波メッシュの最大浸水深からも
同じ凡例で対応させる</li>
<li><b>「dissolve」</b>: 同じキー (例: rank, 用途名, 市町名) を持つポリゴン
を 1 つの巨大ポリゴンにまとめる GIS 操作</li>
<li><b>「sjoin」</b>: spatial join — 点が polygon の中にあるかどうかで属性を
転写する GIS 操作。本レッスンでは盛土点 × 浸水域で利用</li>
</ul>

<h3>結果サマリ (本文の前にざっくり)</h3>
{overall_html}
"""),

    ("使用データ", f"""
<ul class="kv">
<li><b>河川 計画規模 浸水想定 (Shapefile)</b>: {len(flood_plan)} polys, 総面積
{solo_plan_ha:.0f} ha (= {solo_plan_ha/100:.1f} km²)、rank 列あり (浸水深 8 段階)</li>
<li><b>河川 想定最大規模 浸水想定 (Shapefile)</b>: {len(flood_max)} polys, 総面積
{solo_max_ha:.0f} ha (= {solo_max_ha/100:.1f} km²)、rank 列あり</li>
<li><b>津波 想定最大規模 浸水想定 (Shapefile)</b>: 元データ <b>1,256,706 メッシュ
× 4 列</b> (10m 正方メッシュ、各メッシュに最大浸水深 m)。
本レッスンでは初回実行時に <b>rank 8 段階で bin 化 → dissolve</b> して
<code>lessons/assets/L08_tsunami_dissolved.gpkg</code> に保存。
2 回目以降は <b>そのキャッシュを直読</b> ({len(tsunami)} polygons, 総面積
{solo_tsu_ha:.0f} ha)</li>
<li><b>用途地域 GeoJSON (340006 県全域)</b>: {len(landuse)} features
→ 用途別 dissolve {len(landuse_d)} 用途</li>
<li><b>大規模盛土造成地 規制法 xlsx</b>: 受付窓口の市町リスト ({len(ef_munis)} 市町)
を抽出し、各市町の用途地域 polygon 内部に <b>シード固定で 8 点ずつランダム
代表点</b>を生成。本データは <b>位置の illustrative sample</b> であり、
実際の盛土位置を表すものではない (xlsx には座標列が無いため)</li>
<li>CRS は全レイヤ <b>EPSG:6671 (JGD2011 平面直角座標 III 系)</b> に統一し、
面積計算は m² 単位で実施</li>
</ul>

<div class="warn">
<b>津波データの前処理について (要件 S 対策)</b>:
1.25M メッシュをそのまま <code>gpd.overlay()</code> に入れると数十分かかり、
教材としての完走時間 (1〜3 分) を大幅に超える。
本レッスンでは <b>「メッシュの最大浸水深を 8 段階に bin 化 → 各 bin で
unary_union (dissolve)」</b> することで、
1.25M polygon を <b>{len(tsunami)} polygon</b> に集約してから
重ね合わせ計算を行う。
集計の精度はメッシュ単位 (10m) のまま保たれる。
</div>
"""),

    ("ダウンロード (再現用データ・中間データ・図)", """
<p>本レッスンの全成果物に直リンクを置いた。途中ステップから再現したい学習者向け。</p>

<h3>1. 生データ (DoBoX 由来)</h3>
<table>
<tr><th>ファイル</th><th>形式</th><th>備考</th></tr>
<tr><td><code>data/extras/flood_shp/shinsui_keikaku/shinsui_keikakukibo.shp</code></td>
    <td>Shapefile</td><td>河川 計画規模</td></tr>
<tr><td><code>data/extras/flood_shp/shinsui_souteisaidai/shinsui_souteisaidai.shp</code></td>
    <td>Shapefile</td><td>河川 想定最大規模</td></tr>
<tr><td><code>data/extras/tsunami_extracted/.../浸水メッシュ.shp</code></td>
    <td>Shapefile</td><td>津波 1,256,706 メッシュ</td></tr>
<tr><td><code>data/extras/landuse_extracted/340006_*.geojson</code></td>
    <td>GeoJSON</td><td>用途地域 県全域</td></tr>
<tr><td><a href="../data/extras/earth_fill_authorized.xlsx" download>earth_fill_authorized.xlsx</a></td>
    <td>Excel</td><td>大規模盛土規制 受付窓口リスト</td></tr>
</table>

<h3>2. プログラムが生成する中間データ + 図</h3>
<table>
<tr><th>ファイル</th><th>内容</th></tr>
<tr><td><a href="assets/L08_tsunami_dissolved.gpkg" download>L08_tsunami_dissolved.gpkg</a></td>
    <td>津波 1.25M メッシュ → 8 polygon dissolve キャッシュ (初回実行で生成)</td></tr>
<tr><td><a href="assets/L08_overall.csv" download>L08_overall.csv</a></td>
    <td>3 災害 + ダブル + トリプル の総面積</td></tr>
<tr><td><a href="assets/L08_double_city.csv" download>L08_double_city.csv</a></td>
    <td>市町別 ダブルリスク面積</td></tr>
<tr><td><a href="assets/L08_double_yoto.csv" download>L08_double_yoto.csv</a></td>
    <td>用途別 ダブルリスク面積</td></tr>
<tr><td><a href="assets/L08_double_rank_pivot.csv" download>L08_double_rank_pivot.csv</a></td>
    <td>河川 rank × 津波 rank ピボット</td></tr>
<tr><td><a href="assets/L08_rank_compare.csv" download>L08_rank_compare.csv</a></td>
    <td>ランク別 面積 3 災害比較</td></tr>
<tr><td><a href="assets/L08_earthfill_sjoin.csv" download>L08_earthfill_sjoin.csv</a></td>
    <td>盛土サンプル点 × 3 災害 sjoin 結果</td></tr>
<tr><td><a href="assets/L08_multi_overlay_main.png" download>L08_multi_overlay_main.png</a></td>
    <td>図1 三重浸水主題図 (主役)</td></tr>
<tr><td><a href="assets/L08_three_layers_small_multiples.png" download>L08_three_layers_small_multiples.png</a></td>
    <td>図2 個別主題図 small multiples</td></tr>
<tr><td><a href="assets/L08_double_risk_map.png" download>L08_double_risk_map.png</a></td>
    <td>図3 ダブルリスク領域マップ</td></tr>
<tr><td><a href="assets/L08_double_city_bar.png" download>L08_double_city_bar.png</a></td>
    <td>図4 市町別ダブルリスク棒</td></tr>
<tr><td><a href="assets/L08_double_yoto_bar.png" download>L08_double_yoto_bar.png</a></td>
    <td>図5 用途別ダブルリスク棒</td></tr>
<tr><td><a href="assets/L08_rank_compare_bar.png" download>L08_rank_compare_bar.png</a></td>
    <td>図6 ランク別 3 災害比較</td></tr>
<tr><td><a href="assets/L08_double_rank_pivot.png" download>L08_double_rank_pivot.png</a></td>
    <td>図7 ランク × ランク ピボット</td></tr>
<tr><td><a href="assets/L08_earthfill_risk_scatter.png" download>L08_earthfill_risk_scatter.png</a></td>
    <td>図8 盛土点 × ランク 散布</td></tr>
<tr><td><a href="L08_multi_flood_overlay.py" download>L08_multi_flood_overlay.py</a></td>
    <td>再現スクリプト</td></tr>
</table>
"""),

    ("分析1: 津波 1.25M メッシュ → 8 polygon キャッシュ (前処理の主役)", """
<h4>狙い</h4>
<p>津波シェープファイルは <b>1,256,706 行 × 4 列</b> という巨大データ。
そのまま <code>gpd.overlay()</code> や <code>gpd.sjoin()</code> に入れると
数十分〜数時間かかる。教材としての完走時間 (1〜3 分) を守るため、
<b>「メッシュの最大浸水深を 8 段階に bin 化 → 各 bin で dissolve」</b> し、
1.25M を 8 個 (実用上 6 個) の巨大 polygon に集約する。</p>

<h4>手法 (リテラシレベルの直感的説明)</h4>
<ul>
<li><b>bin 化</b>: 連続値の浸水深 (0.46m, 1.03m, 1.99m, ...) を 8 段階の "棚"
に振り分ける操作。ヒストグラムの作り方と同じ。本レッスンでは河川 Shapefile
の rank 列と <b>同じ 8 段階</b> (0-0.5m / 0.5-1m / 1-2m / 2-3m / 3-5m /
5-10m / 10-20m / 20m以上) に揃えることで、河川と津波を直接比較可能にする</li>
<li><b>dissolve</b>: 同じキーを持つ多数の polygon を 1 つの巨大 polygon に統合する操作。
1,000 個の小さな四角形を 1 つの大きな多角形に変える、と思えばよい。
GIS の Boolean union 演算で実装される</li>
<li><b>shapely.box(x0, y0, x1, y1)</b>: 矩形 polygon を 1 行で生成。
1.25M 行に対してベクトル化されているので高速</li>
<li><b>shapely.unary_union(polys)</b>: polygon の配列を 1 つに統合。
内部的に R-tree で空間インデックスを構築し、近接する polygon 同士を併合</li>
</ul>

<h4>入出力 (Before / After 具体例)</h4>
<table>
<tr><th>段階</th><th>このデータで何が起きるか</th><th>サイズ</th></tr>
<tr><td>0. 元データ</td>
    <td>(X=4955, Y=-196005, 最大浸水深=0.46m, 10m 矩形 polygon) という行が 1.25M 行</td>
    <td>(1,256,706 × 4)</td></tr>
<tr><td>1. attr のみ読み込み (geometry 抜き)</td>
    <td>geometry を読まないので 4 倍速い</td>
    <td>1.25M 行 × 3 列 (DataFrame)</td></tr>
<tr><td>2. depth → rank</td>
    <td>0.46m → rank 10、1.03m → rank 30、1.99m → rank 30、...</td>
    <td>1.25M 行に rank 列が追加</td></tr>
<tr><td>3. rank ごと group</td>
    <td>rank 10 が 330,248 行、rank 20 が 282,092 行、...</td>
    <td>6 グループ (rank 70/80 は本データに存在せず)</td></tr>
<tr><td>4. 矩形 polygon 生成</td>
    <td>(X, Y) 中心 ± 5m の矩形を生成。<code>shapely.box()</code> でベクトル化</td>
    <td>各グループ毎に大きさ次第</td></tr>
<tr><td>5. unary_union</td>
    <td>各グループの矩形を 1 つの巨大 polygon に統合。隣接矩形は線として消える</td>
    <td>6 polygons (出力)</td></tr>
<tr><td>6. EPSG:6671 に投影</td>
    <td>津波シェープは独自 tmerc CRS。EPSG:6671 (JGD2011 III 系) に揃える</td>
    <td>同 6 polygons</td></tr>
<tr><td>7. GeoPackage 保存</td>
    <td><code>L08_tsunami_dissolved.gpkg</code> としてキャッシュ</td>
    <td>~2 MB</td></tr>
</table>

<h4>実装</h4>
""" + code('''
import pyogrio, shapely, pandas as pd, geopandas as gpd

# 1. attr のみ読む (geometry を読み飛ばす = 4 倍速)
df = pyogrio.read_dataframe(TSUNAMI_SHP, read_geometry=False)
df.columns = ["x", "y", "depth"]

# 2. 浸水深 → 8 段階 rank
RANK_BINS = [0, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0, 20.0, 999]
RANK_CODES = [10, 20, 30, 40, 50, 60, 70, 80]
df["rank"] = pd.cut(df["depth"], bins=RANK_BINS,
                    labels=RANK_CODES, right=False).astype(int)

# 3. rank ごとに矩形を作って unary_union
out_geoms, out_ranks = [], []
for rk in RANK_CODES:
    sub = df[df["rank"] == rk]
    if len(sub) == 0: continue
    polys = shapely.box(sub["x"]-5, sub["y"]-5, sub["x"]+5, sub["y"]+5)
    out_geoms.append(shapely.unary_union(polys))   # 1.25M → 1 polygon
    out_ranks.append(rk)

gdf = gpd.GeoDataFrame({"rank": out_ranks, "geometry": out_geoms},
                       crs=CUSTOM_TSU_CRS)
gdf.to_crs("EPSG:6671").to_file("L08_tsunami_dissolved.gpkg", driver="GPKG")
''') + f"""

<h4>結果</h4>
<p><b>なぜこの図か</b>: bin 化の効果は数値で語るのが最も明確。
各 rank の polygon 数とサイズを <b>表で見せ</b>、後段の主題図で
"このサイズなら GIS overlay も 1 秒で終わる" ことを実演する。</p>

<table>
<tr><th>処理</th><th>所要時間</th><th>polygon 数</th></tr>
<tr><td>1. <code>pyogrio.read_dataframe(..., read_geometry=False)</code></td>
    <td>3.7 s</td><td>1,256,706 行 (DataFrame)</td></tr>
<tr><td>2. depth → rank bin</td>
    <td>0.5 s</td><td>同上 + rank 列</td></tr>
<tr><td>3. rank=10 (0-0.5m) box + union</td>
    <td>14.7 s</td><td>330,248 → 1 polygon</td></tr>
<tr><td>4. rank=20 (0.5-1.0m) box + union</td>
    <td>13.4 s</td><td>282,092 → 1 polygon</td></tr>
<tr><td>5. rank=30 (1.0-2.0m) box + union</td>
    <td>11.3 s</td><td>331,852 → 1 polygon</td></tr>
<tr><td>6. rank=40 (2.0-3.0m) box + union</td>
    <td>7.1 s</td><td>249,687 → 1 polygon</td></tr>
<tr><td>7. rank=50 (3.0-5.0m) box + union</td>
    <td>1.6 s</td><td>62,567 → 1 polygon</td></tr>
<tr><td>8. rank=60 (5.0-10m) box + union</td>
    <td>0.0 s</td><td>260 → 1 polygon</td></tr>
<tr><td>9. EPSG:6671 投影 + GPKG 保存</td>
    <td>0.6 s</td><td>6 polygons (rank 70/80 なし)</td></tr>
<tr><td><b>合計</b></td><td><b>~53 s (1 回のみ)</b></td><td>1.25M → 6</td></tr>
</table>

<p><b>読み取り</b>:</p>
<ul>
<li>津波の浸水深は <b>rank 10〜40 (0〜3m) が圧倒的多数</b> で、5m 超 (rank 50)
は限定的、10m 超 (rank 70/80) は本データに存在しない。これは
<b>津波が海岸線から内陸へ減衰</b> する物理特性を反映している (H4 への伏線)</li>
<li>1.25M → 6 polygon の集約で、<b>後続の overlay/sjoin が秒単位</b> で終わるようになる
(キャッシュなしだと数十分かかる)。教材で <b>"前処理の重要性"</b> を実体験できる
代表例</li>
<li>キャッシュ <code>.gpkg</code> は ~2 MB と小さく、リポジトリに入れても問題ない</li>
</ul>
"""),

    ("分析2: 三重浸水主題図 (主役の発見)", """
<h4>狙い</h4>
<p><b>河川計画規模 / 河川想定最大規模 / 津波</b> の 3 災害を <b>同一マップに
半透明で重ね</b> 、<b>「重なる場所が複合災害高リスク帯」</b> を視覚的に把握する。
背景に用途地域、点として大規模盛土サンプルも重ねることで、
「リスクの形」と「土地利用」と「人為構造物」を同時に読む。</p>

<h4>なぜこの図か (要件 H)</h4>
<p>3 つの GeoDataFrame を別々の地図に並べると比較に上下スクロールが必要。
<b>同一座標系・同一スケールで透過重ねした 1 枚</b> なら、
赤紫 (河川+津波の重なり) と単色 (片方だけ) の <b>違いが目で識別できる</b>。
これは <b>choropleth + 透明度</b> という GIS の基本パターン。</p>

<h4>実装</h4>
""" + code('''
fig, ax = plt.subplots(figsize=(13, 9))
landuse_d_c.plot(ax=ax, color="#eeeeee", alpha=0.6)               # 背景
flood_plan_c.plot(ax=ax, color="#56a4dc", alpha=0.55)              # 河川計画
flood_max_c.plot(ax=ax, color="#0e4282", alpha=0.45)               # 河川最大
for rk in sorted(tsunami["rank"].unique()):                        # 津波(深さ別)
    tsunami[tsunami["rank"]==rk].plot(ax=ax, color=tsu_color[rk], alpha=0.55)
earth_fill.plot(ax=ax, color="black", markersize=22)               # 盛土点
''') + f"""

<h4>結果</h4>
""" + figure("assets/L08_multi_overlay_main.png",
              "広島県沿岸 三重浸水主題図 (河川計画 + 河川最大 + 津波 + 盛土点)") + f"""
<p><b>読み取り</b>:</p>
<ul>
<li><b>太田川河口デルタ</b> (広島市中区/西区/南区) では、河川と津波の
両方が指定されている領域が広く、<b>赤紫がかった部分</b> がダブルリスク帯</li>
<li>沿岸の港湾部 (呉市・広島市南区・廿日市市など) でも、
河川と津波の重なりが帯状に出現</li>
<li>大規模盛土サンプル (黒点) のうち、<b>沿岸/平野部に分布</b> するものは
浸水帯と重なる位置にある</li>
<li>河川単独 (青のみ) は内陸の谷筋に伸びる。津波単独 (橙のみ) は海岸線から
内陸 1km 程度にとどまる。両者の <b>境界帯がダブルリスク</b></li>
</ul>
"""),

    ("分析3: 個別主題図 small multiples (3 災害比較)", """
<h4>狙い</h4>
<p>重ね合わせ図 (主役) では各災害の <b>個別の姿</b> が見えにくい。
3 つを同じスケール・同じ深さ凡例で並べた <b>small multiples</b> で
"単独の形" を比較し、重ね合わせの解釈を補強する。</p>

<h4>なぜこの図か</h4>
<p>1 枚に 3 つを重ねるのが主役なら、3 枚に分けるのは脇役。
両方あって初めて「重なりの読み」と「単独の読み」が両立する。
<b>条件 (= 災害種類) だけ変えて並べる small multiples</b> は比較用途で最適。</p>

""" + figure("assets/L08_three_layers_small_multiples.png",
              "(a) 河川計画 (b) 河川想定最大 (c) 津波想定最大 — 同一スケール") + f"""
<p><b>読み取り</b>:</p>
<ul>
<li>(a) 河川計画規模は <b>河道に沿った細長い帯</b>。氾濫域は限定的</li>
<li>(b) 河川想定最大規模は計画の <b>2 倍以上</b> に拡大、谷筋・平野部に広がる</li>
<li>(c) 津波は <b>海岸線に沿った帯</b> で、内陸への侵入は限定的だが
広島市・呉市の港湾部では深く入る</li>
<li>3 図を比較すると、<b>"河川は内陸+山地、津波は沿岸"</b> という棲み分けが
見える。両者の交差点 (デルタ・低地) がダブルリスク帯</li>
</ul>
"""),

    ("分析4: ダブルリスク領域 (河川想定最大 ∩ 津波) の計算と可視化", f"""
<h4>狙い</h4>
<p>重ね合わせ主題図は <b>定性的</b> で「どこ」は分かるが「何 ha」は分からない。
<b><code>gpd.overlay(A, B, how='intersection')</code> で正確に交差面積を出す</b>
ことで、政策判断に使える定量値に変換する。</p>

<h4>手法 (黒箱化)</h4>
<table>
<tr><th>関数</th><th>入力</th><th>出力</th></tr>
<tr><td><code>gpd.overlay(A, B, how='intersection')</code></td>
    <td>2 つの GeoDataFrame</td>
    <td>両方に含まれる部分の polygon 群</td></tr>
<tr><td><code>geometry.unary_union</code></td>
    <td>polygon の Series</td>
    <td>1 つの巨大 polygon (重複部分は併合)</td></tr>
<tr><td><code>geometry.area</code></td>
    <td>polygon (CRS が m 単位)</td>
    <td>面積 (m²)</td></tr>
</table>
<p><b>ツール化の意味</b>: 内部で R-tree 空間インデックス、トポロジ修正、
Boolean 演算が走るが、利用者は知らなくても結果が得られる。
原理 (集合の交差) を理解した上で <b>ブラックボックスとして利用</b> するのが
GIS の実務スタイル。</p>

<h4>実装</h4>
""" + code('''
# 各レイヤを単一 polygon にまとめる
flood_max_union = gpd.GeoDataFrame(
    {"id":[0]}, geometry=[flood_max.geometry.unary_union], crs=TARGET_CRS)
tsunami_union   = gpd.GeoDataFrame(
    {"id":[0]}, geometry=[tsunami.geometry.unary_union],  crs=TARGET_CRS)

# ダブル: 河川最大 ∩ 津波
double = gpd.overlay(flood_max_union, tsunami_union,
                     how="intersection", keep_geom_type=False)
double_ha = double.geometry.area.sum() / 10000  # ha

# トリプル: 河川計画 ∩ 河川最大 ∩ 津波 (計画 ⊂ 最大 なので実質 計画 ∩ 津波)
triple = gpd.overlay(double, flood_plan_union,
                     how="intersection", keep_geom_type=False)
''') + f"""

<h4>結果</h4>
<p><b>なぜこの図か</b>: ダブル領域だけを <b>赤色</b> でハイライトした
主題図にすると、政策資料としてそのまま使える明快さになる。
背景に河川単独 (青) と津波単独 (橙) を薄く描くことで、ダブル領域の
"位置付け" も同時に伝わる。</p>

""" + figure("assets/L08_double_risk_map.png",
              f"ダブル浸水リスク領域 (赤): 河川想定最大 ∩ 津波 = {double_ha:.0f} ha") + f"""

<table>
<tr><th>領域</th><th>面積 (ha)</th><th>面積 (km²)</th></tr>
<tr><td>河川 計画規模 単独</td><td>{solo_plan_ha:.1f}</td><td>{solo_plan_ha/100:.2f}</td></tr>
<tr><td>河川 想定最大 単独</td><td>{solo_max_ha:.1f}</td><td>{solo_max_ha/100:.2f}</td></tr>
<tr><td>津波 想定最大 単独</td><td>{solo_tsu_ha:.1f}</td><td>{solo_tsu_ha/100:.2f}</td></tr>
<tr><td><b>ダブル (河川最大 ∩ 津波)</b></td>
    <td><b>{double_ha:.1f}</b></td><td><b>{double_ha/100:.2f}</b></td></tr>
<tr><td>トリプル (計画 ∩ 最大 ∩ 津波)</td>
    <td>{triple_ha:.1f}</td><td>{triple_ha/100:.2f}</td></tr>
</table>

<p><b>読み取り</b>:</p>
<ul>
<li>ダブルリスク領域は <b>{double_ha:.0f} ha</b> ({double_ha/100:.2f} km²)。
これは {('100 ha 以上 → H1 支持' if double_ha >= 100 else '100 ha 未満 → H1 反証')}</li>
<li>計画 ⊂ 最大 という包含関係があるので、トリプルは実質「計画 ∩ 津波」と同義
({triple_ha:.0f} ha)。<b>ダブルの一部</b> がトリプル</li>
<li>ダブル領域は河川単独 ({solo_max_ha:.0f} ha) の {double_ha/solo_max_ha*100:.1f}%、
津波単独 ({solo_tsu_ha:.0f} ha) の {double_ha/solo_tsu_ha*100:.1f}%。
両者が "出会う狭い帯" であることが定量化された</li>
</ul>
"""),

    ("分析5: ダブルリスク × 市町別分布", """
<h4>狙い</h4>
<p>"どこにダブルリスクが集中しているか" を市町粒度で見る。
H2 (広島市南区/西区港湾部集中) を検証する分析。</p>

<h4>なぜこの図か</h4>
<p>市町数が 25 程度あるので、地図に 25 色で塗ると視覚混乱する。
<b>横棒グラフで上位 12 市町をランキング</b> する方が、
"どの市町に集中したか" を一目で読み取れる。</p>

<h4>手法</h4>
""" + code('''
# 市町別 dissolve
city_d = landuse[["city_name","geometry"]].dissolve(by="city_name").reset_index()

# 各市町 × ダブル領域 の交差面積
for _, row in city_d.iterrows():
    inter = row.geometry.intersection(double.geometry.unary_union)
    a_ha = inter.area / 10000
    ...
''') + """

""" + figure("assets/L08_double_city_bar.png",
              "市町別 ダブル浸水リスク領域 (上位 12)") + f"""
{city_top_html}
<p><b>読み取り</b>:</p>
<ul>
<li>ダブルリスク面積 1 位は <b>{top_double_city}</b>
({city_double_df.iloc[0]['double_ha']:.0f} ha,
{city_double_df.iloc[0]['double_pct']:.1f}%)</li>
<li>{('広島市南区/西区/中区が上位 → H2 支持' if any('広島市' in c
    for c in city_double_df['city'].head(5).tolist())
   else '広島市以外が首位 → H2 部分支持に留まる')}</li>
<li>沿岸の港湾市町 (呉市・廿日市市・大竹市など) も上位に入り、
<b>太田川河口に限らない</b> 沿岸全域の課題であることが見える</li>
<li>市町総面積に対する比率 (%) で見ると、絶対面積では小さい市町でも
"市町の数 % が複合浸水域" という質的危機度が浮かぶ</li>
</ul>
"""),

    ("分析6: ダブルリスク × 用途地域", """
<h4>狙い</h4>
<p>H5 (工業/商業/港湾系に集中) を検証。土地利用視点でダブルリスク領域の
"中身" を分解する。</p>

<h4>なぜこの図か</h4>
<p>市町ランキング (分析5) は <b>"どこ"</b>、用途ランキング (本分析) は
<b>"何が"</b>。両軸を並べて初めて「広島市南区の工業地が」のような
2 軸クロスの解釈が成り立つ。</p>

""" + figure("assets/L08_double_yoto_bar.png",
              "用途地域 × ダブル浸水リスク 上位用途") + f"""
{yoto_top_html}
<p><b>読み取り</b>:</p>
<ul>
<li>ダブルリスク面積 1 位の用途は <b>{top_yoto_double}</b>
({yoto_summary.iloc[0]['overlap_ha']:.0f} ha,
{yoto_summary.iloc[0]['double_pct']:.1f}%)</li>
<li>{('工業/工業専用/準工業/商業 が上位 → H5 支持' if any(k in top_yoto_double
    for k in ['工業','商業','準工業'])
   else '住居系が上位 → H5 反証 (沿岸住居の方がダブルリスクに多い)')}</li>
<li>住居系の絶対面積も大きい場合は、<b>「臨海工業地に加えて、海沿いに住む人にも
ダブルリスク」</b> が共存することを示唆</li>
</ul>
"""),

    ("分析7: ランク別 面積比較 (河川 vs 津波)", """
<h4>狙い</h4>
<p>H4 (河川は深い、津波は浅瀬中心) を検証。同じ rank 8 段階凡例で、
3 災害の浸水深分布を <b>対数スケール棒グラフ</b> で比較する。</p>

<h4>なぜこの図か</h4>
<p>線形スケールだと浅瀬 (rank 10-20) の桁違いの大きさに圧縮されて、
深い帯 (rank 50-80) が読み取れない。<b>対数スケール</b> で揃えれば、
全 rank の分布の "形" が見える。</p>

""" + figure("assets/L08_rank_compare_bar.png",
              "浸水深ランク別 面積 (河川計画 vs 河川最大 vs 津波) — 対数") + f"""
{rank_cmp_html}
<p><b>読み取り</b>:</p>
<ul>
<li>河川は <b>rank 50 (3-5m) や rank 60 (5-10m) でも一定面積</b> が存在し、
山地の谷で浸水深が深くなる傾向 (河川の物理特性: 河道の絞り込みで水位上昇)</li>
<li>津波は <b>rank 50 まで</b> に大半が収まり、rank 60 以上はわずか。
rank 70-80 は本データに <b>存在しない</b>。これは津波の物理特性
(海岸線から内陸へ減衰、市街地の摩擦で深さ低下) を反映</li>
<li>{('深部 (rank ≥ 50) 面積: 河川 > 津波 → H4 支持' if H4_river_deep > H4_tsu_deep
   else 'H4 反証')}</li>
<li>計画規模と想定最大規模で全 rank が揃って増えており、
<b>「想定の規模拡大は浸水域全域で起こる」</b> ことが確認できる</li>
</ul>
"""),

    ("分析8: 河川 rank × 津波 rank ピボット", """
<h4>狙い</h4>
<p>ダブルリスク領域内で、<b>「河川は何 m + 津波は何 m が組み合わさるか」</b>
を 8 × 8 ピボット表で見る。これは "リスクの中身" を最も詳細に見る分析。</p>

<h4>なぜこの図か</h4>
<p>2 軸の値を一目で把握するヒートマップは、ピボット表の自然な可視化。
セル数値も併記することで、表として読める/図として読めるの両立。</p>

""" + figure("assets/L08_double_rank_pivot.png",
              "ダブルリスク領域: 河川 rank × 津波 rank の重なり面積 (ha)") + f"""
{pivot_html}
<p><b>読み取り</b>:</p>
<ul>
<li>ダブルリスクの <b>主役は浅瀬同士の重なり</b> (河川 rank 10-30 × 津波 rank 10-30)</li>
<li>河川深 + 津波浅 (例: 河川 rank 50 × 津波 rank 10) のセルがあれば、
<b>河川氾濫で 3-5m 沈む土地に津波 0.5m 未満が来る</b> という複雑シナリオ</li>
<li>津波 rank が高い + 河川 rank が低いセルは、海岸線で河川氾濫が及ばないが
津波が深い領域 (純粋な海寄り)</li>
</ul>
"""),

    ("分析9: 大規模盛土サンプル × 3 災害 sjoin", f"""
<h4>狙い</h4>
<p>盛土点 ({len(ef_pts_df)} 点) のうち、<b>浸水想定域内にあるもの</b> がいくつか?
そのうちダブルリスク (河川+津波) はいくつか? という H3 の検証。</p>

<h4>手法 (sjoin = spatial join)</h4>
<ul>
<li><b>sjoin</b>: 点が polygon の中にあるかで属性を付与する GIS 操作。
<code>predicate='within'</code> を指定すると "点 ⊂ polygon" を判定</li>
<li>本レッスンでは <code>geometry.intersects()</code> を使った愚直な
ループでも同じ結果が得られる (点の数が少ないので速度差は無視できる)</li>
<li>各盛土点について 3 災害それぞれの最大 rank を取得 → DataFrame 列に追加</li>
</ul>

<h4>実装</h4>
""" + code('''
def lookup_rank(point, gdf):
    sub = gdf[gdf.intersects(point)]
    return int(sub["rank"].max()) if not sub.empty else None

ef_pts_df["rank_river_max"]  = [lookup_rank(p, flood_max)  for p in earth_fill.geometry]
ef_pts_df["rank_river_plan"] = [lookup_rank(p, flood_plan) for p in earth_fill.geometry]
ef_pts_df["rank_tsunami"]    = [lookup_rank(p, tsunami)    for p in earth_fill.geometry]
''') + f"""

<h4>結果サマリ</h4>
{ef_summary_html}

""" + figure("assets/L08_earthfill_risk_scatter.png",
              "盛土サンプル × ランク × ランク 散布") + f"""
<p><b>読み取り</b>:</p>
<ul>
<li>サンプル {len(ef_pts_df)} 点中、<b>{n_in_max} 点</b> が河川想定最大規模に入る、
<b>{n_in_tsu} 点</b> が津波想定に入る、<b>{n_in_double} 点</b> がダブル</li>
<li>{('ダブルリスク内に盛土が存在 → H3 支持' if n_in_double >= 1
    else 'ダブルリスク内の盛土無し → H3 反証 (ただしサンプルの位置が illustrative なので参考)')}</li>
<li>散布図で原点 (-5, -5) 付近 (どの災害にも入らない点) は内陸/山地に位置する盛土。
赤点 (ダブル) は <b>低地の港湾部</b> に集中</li>
</ul>

<h4>サンプル盛土の先頭 15 件</h4>
{ef_pts_html}
<p class="tnote">本サンプル点は xlsx の受付窓口市町から各 8 点ずつ
シード固定で生成した illustrative データ。実際の盛土位置を
表すものではない。位置データが取得できた場合は同じ sjoin 手順で
正確な判定が可能。</p>
"""),

    ("仮説検証と考察", f"""
<h3>仮説と結果の照合</h3>
{verdict_html}

<h3>考察</h3>
<ul>
<li><b>3 ハザードの空間関係</b>: 河川 (内陸方向に伸びる) と津波 (沿岸方向に伸びる) は
本来別の物理現象だが、<b>太田川河口デルタや沿岸低地</b> という共通の地理舞台で
重なる。これがダブルリスク帯 ({double_ha:.0f} ha) の正体</li>
<li><b>津波の浅瀬中心の分布</b>: rank 10-40 (3m 未満) が津波域の大半を占め、
深い帯 (rank 50 以上) は限定的。これは 津波のシミュレーション物理 (海岸線で
減衰) を反映 (H4 支持)</li>
<li><b>用途別の含意</b>: ダブルリスク域に <b>{top_yoto_double}</b> が最多。
{('臨海工業地と港湾物流に対する複合災害対策' if any(k in top_yoto_double
   for k in ['工業','商業','準工業'])
   else '住居系が最多なら、人命保護の観点で防災投資の優先度がさらに高まる')}</li>
<li><b>盛土の複合リスク</b>: 大規模盛土は本来 <b>地震時の崩壊</b> が主リスクだが、
浸水域内にあれば <b>液状化 + 浸水 + 崩壊</b> の三重連鎖シナリオが想定される。
H3 の {n_in_double} 点はサンプル数として小さいが、設計上要警戒の存在を示す</li>
<li><b>計画 vs 想定最大の差</b>: 河川では計画 ({solo_plan_ha:.0f} ha) と最大
({solo_max_ha:.0f} ha) で {solo_max_ha/max(solo_plan_ha,1):.1f} 倍の差。
"計画規模で安全だから大丈夫" は誤り、想定最大規模で考えるべき</li>
</ul>

<h3>政策含意 (Policy Implications)</h3>
<ul>
<li><b>避難計画の二重化</b>: ダブルリスク市町 ({top_double_city} 等) では
河川氾濫避難経路と津波避難経路が一致しないケースがある。
両方に対応した <b>多層避難計画</b> が必要</li>
<li><b>盛土設計基準の重ね合わせ</b>: 浸水域内の大規模盛土は、
通常の盛土設計に加えて <b>耐浸水/排水設計</b> を要件化する余地</li>
<li><b>用途地域の見直し</b>: ダブルリスク領域に住居系が多い場合、
新規宅地化を抑制し、既存住居の <b>嵩上げ/移転</b> 支援を検討</li>
</ul>
"""),

    ("発展課題 (結果から導かれる新たな問い)", f"""
<ol>
<li><b>盛土実位置データへの拡張</b>:
  <ul>
  <li>結果X: 盛土サンプル {len(ef_pts_df)} 点中、ダブルリスク内 {n_in_double} 点
  (illustrative)</li>
  <li>新仮説Y: 実際の許可・届出盛土 (#1429, #1430) のうち <b>X%</b> が
  ダブルリスク域内にある</li>
  <li>課題Z: DoBoX #1429/#1430 の届出データ (位置情報含む) を取得し、
  本レッスンと同じ sjoin パイプラインを実行して X を測定</li>
  </ul>
</li>

<li><b>避難所との 4 重交差</b>: 本レッスンのダブルリスク領域に
<b>避難所 (DoBoX #42)</b> が含まれていないか? 含まれている避難所は
"自分が浸水するのに人を受け入れる" という矛盾を抱える</li>

<li><b>標高との交差</b>: ダブルリスク領域の <b>標高 (DoBoX 標高 5m メッシュ)</b>
分布を見て、"標高 X m 以下なら必ずダブル" という閾値を導出</li>

<li><b>建物密度との交差</b>: 都市計画基礎調査の建物密度と本ダブルリスク領域を
クロスし、<b>「人がたくさん住んでいるダブルリスク域」</b> を特定</li>

<li><b>過去災害履歴との照合</b>: 平成 30 年 7 月豪雨や昭和 21 年枕崎台風の
浸水実績と本想定の整合を確認 (想定の妥当性検証)</li>

<li><b>市町別 small multiples</b>: 本レッスンは県全域 1 枚だが、
広島市・呉市・福山市など <b>市町別に拡大した small multiples</b> を作成し、
"自分の街のダブルリスク" を直感的に把握できる教材に拡張</li>

<li><b>X09 との結合</b>: X09 (河川浸水 × 用途地域) と本レッスンを統合し、
<b>「河川単独」「ダブル」「津波単独」</b> の用途別比率を 1 枚の積み上げ棒で見る</li>
</ol>
"""),

    ("補足: GIS 関数の黒箱化早見表 / 処理時間とメモリ", f"""
<h4>本レッスンで使った GIS 関数 (要件 J: ツール化視点)</h4>
<table>
<tr><th>関数</th><th>入力</th><th>出力</th><th>このレッスンでの用途</th></tr>
<tr><td><code>gpd.read_file(path)</code></td><td>Shapefile / GeoJSON</td>
    <td>GeoDataFrame</td><td>河川/用途地域の読み込み</td></tr>
<tr><td><code>pyogrio.read_dataframe(p, read_geometry=False)</code></td>
    <td>Shapefile</td><td>属性のみ DataFrame</td>
    <td>津波 1.25M メッシュの高速読み込み (geometry スキップ)</td></tr>
<tr><td><code>gdf.to_crs("EPSG:6671")</code></td><td>GeoDataFrame</td>
    <td>同 (CRS 変換済)</td><td>3 レイヤを同一 CRS に揃える</td></tr>
<tr><td><code>shapely.box(x0,y0,x1,y1)</code></td>
    <td>numpy 配列 4 本</td><td>polygon 配列</td>
    <td>津波メッシュ (X,Y) 中心 ±5m の矩形を一括生成</td></tr>
<tr><td><code>shapely.unary_union(polys)</code></td>
    <td>polygon の配列</td><td>1 つの統合 polygon</td>
    <td>同 rank の矩形を 1 つに dissolve</td></tr>
<tr><td><code>gdf.dissolve(by='col')</code></td>
    <td>GeoDataFrame + キー列</td>
    <td>キー単位で union された GeoDataFrame</td>
    <td>用途別/市町別/rank別 集約</td></tr>
<tr><td><code>gpd.overlay(A, B, how='intersection')</code></td>
    <td>2 GeoDataFrame</td><td>交差ポリゴン</td>
    <td>河川 ∩ 津波 (ダブルリスク)、河川 ∩ 用途</td></tr>
<tr><td><code>gpd.sjoin(pts, polys, predicate='within')</code></td>
    <td>点 GDF + polygon GDF</td>
    <td>点に polygon 属性を付与</td>
    <td>盛土点が浸水域内かを判定</td></tr>
<tr><td><code>geom.buffer(0)</code></td>
    <td>polygon</td><td>polygon</td>
    <td>微小トポロジ崩れの修正 (TopologyException 予防)</td></tr>
<tr><td><code>geom.area</code></td>
    <td>polygon (m 単位 CRS)</td><td>面積 (m²)</td>
    <td>ha = m² / 10,000 で換算</td></tr>
</table>

<h4>処理時間プロファイル (要件 S 対応)</h4>
<table>
<tr><th>段階</th><th>時間 (1 回目)</th><th>時間 (2 回目以降)</th></tr>
<tr><td>津波 dissolve キャッシュ作成</td><td>~53 s</td><td><b>0 s (skip)</b></td></tr>
<tr><td>河川 + 用途地域 読み込み + 投影</td><td>~5 s</td><td>~5 s</td></tr>
<tr><td>津波キャッシュ読み込み</td><td>~1 s</td><td>~1 s</td></tr>
<tr><td>用途別 dissolve</td><td>~2 s</td><td>~2 s</td></tr>
<tr><td>盛土サンプル生成</td><td>~3 s</td><td>~3 s</td></tr>
<tr><td>主役マップ + small multiples 描画</td><td>~30 s</td><td>~30 s</td></tr>
<tr><td>ダブルリスク overlay + 集計</td><td>~10 s</td><td>~10 s</td></tr>
<tr><td>ピボット (8×8 intersection)</td><td>~5 s</td><td>~5 s</td></tr>
<tr><td><b>合計</b></td><td><b>~110 s = 1.8 分 (要件S OK)</b></td>
    <td><b>~57 s = 1 分弱 (キャッシュ後)</b></td></tr>
</table>

<p><b>キャッシュ戦略の意味</b>: 1.25M メッシュ → 6 polygon は
<b>情報量を捨てるトレードオフ</b> ではなく、教材で扱う粒度 (rank 8 段階) では
<b>情報損失なし</b>。むしろメッシュ単位の冗長性を除去している。
この種の前処理は GIS 実務でも常套手段で、「重い生データを軽量サマリに落として
パイプラインに乗せる」が定石。</p>

<h4>本レッスンの限界</h4>
<ul>
<li><b>盛土位置の illustrative 性</b>: xlsx には座標が無いため、
受付市町ごとに用途地域内ランダム代表点を生成。実位置データ (DoBoX #1429/#1430)
を使えば同じ sjoin で正確な判定が可能</li>
<li><b>3D 浸水深の平均化</b>: 河川 rank と津波 rank は両方 8 段階に揃えたが、
河川は <b>谷地形での集中</b>、津波は <b>海面上昇による拡散</b> という物理が異なる。
"同じ rank 50 でも、河川と津波で危険度の質は違う" 点は要注意</li>
<li><b>同時発生の前提</b>: 河川氾濫と津波が同時に起こる確率は本レッスンでは
扱っていない (空間的重なりのみ)。時間軸の連動 (例: 大地震 → 津波 + ダム決壊
→ 河川氾濫) は別レッスン課題</li>
</ul>
"""),
]

html = render_lesson(
    num=8,
    title="L08 河川浸水 × 津波浸水 × 大規模盛土 — 複合災害リスクの重ね合わせ",
    tags=["L系", "GIS", "重ね合わせ", "複合災害", "津波", "河川"],
    time="60-75 分",
    level="リテラシ + GIS 中級",
    data_label="河川 Shapefile (rank 列) + 津波メッシュ (1.25M) + 用途地域 + 盛土規制 xlsx",
    sections=sections,
    script_filename="lessons/L08_multi_flood_overlay.py",
)
html = html.replace("<body>", '<body data-draft="1" data-stier="L">', 1)
out = LESSONS / "L08_multi_flood_overlay.html"
out.write_text(html, encoding="utf-8")
print(f"\n[HTML] {out.name} を出力 ({out.stat().st_size:,} bytes)")
print(f"\n=== 全フェーズ完了: {time.time()-t0:.1f}s ===")
