# -*- coding: utf-8 -*-
"""L47 水害リスクマップ（浸水頻度図）単独 3 研究例分析
       — 広島県 32 河川水系の「年超過確率 1/5〜1/100 + 想定最大規模」7 段階リスクマップを
         3 つの独立した研究角度で読み解く

カバー宣言:
  本記事は DoBoX のシリーズ「水害リスクマップ（浸水頻度図）」 1 件
  (dataset_id = 1640) を <b>単独</b>で取り上げ、
  広島県内の 32 河川水系 × 3 浸水深閾値 = 24 PDF (うち 23 件取得可)
  を <b>3 つの独立した研究角度（RQ1/RQ2/RQ3）</b>で並列に分析する。

  「水害リスクマップ（浸水頻度図）」とは:
    広島県土木建築局河川課が、流域治水の推進を目的として
    年超過確率 1/5・1/10・1/30・1/50・1/70・1/100 と「想定最大規模」の
    <b>計 7 段階の降雨シナリオ</b>で想定浸水範囲を重ね、
    1 枚の地図に「色分け」して表示した A3 横の PDF 図面である。

  「浸水想定図 (河川 L1=計画規模 / L2=想定最大規模)」が
  <b>2 規模 (計画規模 + 想定最大規模)</b>で固定されているのに対し、
  本マップは <b>頻度ベースで 7 段階</b>に細分化して表現する。
  「100 年に 1 度クラス (1/100) はここまで」「30 年に 1 度クラス (1/30)
  はここまで」が同一図上で<b>同時に判別できる</b>のがこのマップの本質。

  さらに、<b>浸水深 3 閾値</b>の別図で公開される:
    - 浸水深 0.0m 以上 (浸水あり)
    - 浸水深 0.5m 以上 (床上浸水相当)
    - 浸水深 3.0m 以上 (一階居室浸水相当)
  これにより「どの頻度・どの深さで・どこが浸水するか」を 3 軸で読める。

  本記事は L4-L11 (河川浸水想定図), L44 (高潮浸水想定), L45 (ため池浸水想定)
  と<b>厳密に独立</b>。それらが Shapefile/ラスタの幾何データなのに対し、
  本データは <b>PDF (ベクタ図面)</b> として配布される、フォーマットの異なる
  浸水情報である点が研究的に重要。

研究の問い (3 RQ):
  RQ1 (主研究): 水害リスクマップの<b>仕組み</b>とは何か？ 7 階級 (1/5〜想定最大規模)
       の「色面」は、PDF 図面上でどう配分されているか？ 32 河川水系 × 3 深さ閾値の
       <b>23 PDF</b>で、頻度階級と深さの組合せはどう変化するか？
  RQ2 (副研究 1): 高リスクエリア (= 高頻度=低確率閾値=1/5, 1/10, 1/30 で
       浸水するエリア) の<b>地理特性</b>はどうなっているか？ 河川水系別に
       見た時、リスクの大きい水系はどこか？ <b>規模 (面積) ベース</b>で水系を
       ランキングすると、32 水系のうち上位は何か？
  RQ3 (副研究 2): <b>既存浸水想定 (L4-L11 河川浸水想定図 / L44 高潮 / L45 ため池)</b>
       との関係はどうなっているか？ 水害リスクマップは既存の浸水想定と
       <b>地理的にどう重なるか</b>。「Shapefile データはあるが PDF にない」
       「逆」のエリアはどこか。「フォーマットの違い」が情報の<b>欠落</b>を
       生んでいないか？

仮説 (3 RQ × 主仮説 5):
  H1 (頻度階級の積層構造, RQ1): 1/5 (高頻度=狭) → 1/100 (中) → 想定最大規模 (広)
       の順にリスクマップ上の塗り面積が単調増加する。1/5 で塗られたピクセルは
       必ず 1/100 や想定最大規模でも塗られる (=入れ子=積層構造)。
       例外があるとすればそれは<b>制度的な切れ目</b>を示唆する。
  H2 (深さ閾値による減衰, RQ1): 浸水深 0.0m 以上 (有) → 0.5m 以上 → 3.0m 以上
       の順にリスクマップ上の塗り面積が単調減少する。3.0m 以上に達するエリアは
       0.0m 以上の <b>1/3 以下</b>で、深い浸水は<b>河川直近に局在</b>する。
  H3 (水系規模の偏り, RQ2): 32 水系のうち、浸水範囲面積で<b>上位 5 水系</b>が
       全体の 50% 以上を占める。長い本流をもつ <b>瀬野川・賀茂川・芦田川 (山南川等)</b>
       は中小水系より圧倒的に大きい (= 浸水想定面積はリーダー水系に集中)。
  H4 (PDF と Shapefile の被覆差, RQ3): L4-L11 で扱う Shapefile 浸水想定 (計画規模 +
       想定最大規模) は<b>2 段階のみ</b>で、本リスクマップの 1/5・1/10・1/30・1/50・
       1/70 という<b>中間頻度 5 段階</b>の情報は Shapefile では<b>欠落</b>している。
       PDF を読まないとアクセスできない情報が大量にある。
  H5 (頻度マップの本質, RQ1): 「水害リスクマップ」は <b>面の集合</b>ではなく
       <b>頻度の地図</b>である。同じ場所に「1/5 で浸水」と「1/100 で浸水」の
       2 情報がある時、それは<b>「最頻=1/5 の塗り色」</b>として表現される
       (=最も厳しい頻度が表示されている)。これは PDF レイヤ重ね順の制約。

要件 S 準拠:
  - 23 PDF を 100 dpi でラスタ化して 7 階級の色マッチング集計。
  - 1 PDF あたり ~A3 (3308×2339 px @300dpi) → 100 dpi (1102×779 px) で十分。
    1 PDF 集計 ~1-2 秒、合計 ~30-60 秒 (要件 S 内)。
  - 集計結果は <code>data/extras/L47_flood_risk_map/_cache/pdf_color_hist.json</code> に
    キャッシュ。2 回目以降は <1 秒で再現。
  - 既存浸水想定 Shapefile (L4-L11 の <code>shinsui_souteisaidai.shp</code>) との
    比較は dissolve 済みデータを bbox で絞って 1 度だけ overlay する。

要件 T 準拠 (位置情報あり = 地図必須):
  - PDF プレビュー画像 + 凡例カラー解説 (図 1)
  - 7 階級 × 8 リソースの面積積層 stacked bar (図 2)
  - 3 浸水深閾値での面積比較 small multiples (図 3)
  - 水系規模ランキング (図 4)
  - リスクマップ vs 既存 Shapefile の bbox 重ね合わせマップ (図 5)
  - 河川水系名から推定する地理ヒート (図 6)
  - 7 階級ピクセル比率の small multiples (図 7-8)

要件 Q 準拠: 図 8+ / 表 11+ (3 RQ × 多角度: 階級別 / 深さ別 / 水系別 / 比較).

データ仕様:
  - dataset 1640: 8 リソース = 8 河川水系グループ (合計 32 河川水系を含む)
  - 形式: PDF (ベクタ A3 横, 1190×842 pt = 約 8255×5840 px @72dpi×7倍)
  - ZIP 合計サイズ: ~98 MB (圧縮後)
  - PDF 合計: 24 (うち 1 件 = 八幡川 0.0m PDF はサーバ ZIP 破損で読めず)
  - 公表年月日: 令和6年4月10日 (2024-04-10)
  - 作成主体: 広島県土木建築局河川課
  - ライセンス: CC-BY 4.0
  - 凡例 (測色済み):
      1/5 = (200,179,223) 紫薄
      1/10 = (246,140,140) 桃濃
      1/30 = (246,188,189) 桃淡
      1/50 = (151,207,246) 水色
      1/70 = (182,249,177) 緑
      1/100 = (255,200,84) 橙
      想定最大規模 = (255,255,179) 淡黄
"""
from __future__ import annotations
import sys, time, json, os, io as _io, re
from pathlib import Path

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, Rectangle
from matplotlib.colors import ListedColormap, BoundaryNorm
from matplotlib.lines import Line2D
import zipfile

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

t0 = time.time()
print("=== L47 水害リスクマップ（浸水頻度図）単独 3 研究例分析 ===", flush=True)

# =============================================================================
# 0. 定数・データパス
# =============================================================================
DATA_DIR = ROOT / "data" / "extras" / "L47_flood_risk_map"
SAMPLES = DATA_DIR / "samples"
EXTRACT = DATA_DIR / "extracted"
CACHE   = DATA_DIR / "_cache"
CACHE.mkdir(parents=True, exist_ok=True)

# 8 リソース (DoBoX dataset 1640) のメタ
# rid: resource_id, slug: ローカル展開ディレクトリ名, jp: 含む水系の和名
RESOURCES = {
    176947: ("nagata_okajino_tanaka",      "永田川・小鹿野川・田中川 (3水系)"),
    176948: ("kamo_honkawa",               "賀茂川・本川 (2水系)"),
    176949: ("takada_ocho_harada_etc",     "高田川・大長川・原田川・原下川・小原川 (5水系)"),
    176950: ("sannan_motoya_tejiro",       "山南川・本谷川・手城川 (3水系)"),
    176951: ("seno",                       "瀬野川 (1水系)"),
    176952: ("fujii_hongo_habara_etc",     "藤井川・本郷川・羽原川・新川・栗原川・大田川・才戸川・大河原川 (8水系)"),
    176953: ("yahata_eikoji_mitearai_etc", "八幡川・永慶寺川・御手洗川・可愛川・岡ノ下川 (5水系)"),
    176954: ("noro_kinoya_takano_etc",     "野呂川・木谷郷川・高野川・蛇道川・三津大川 (5水系)"),
}
# 合計水系数 (3+2+5+3+1+8+5+5 = 32)
TOTAL_RIVERS = sum(int(re.search(r"\((\d+)", v[1]).group(1)) for v in RESOURCES.values())
print(f"  対象 河川水系総数: {TOTAL_RIVERS}")

# 3 段階の浸水深閾値 (PDF ファイル名に対応)
DEPTH_FILES = [
    ("d00", "0.0m以上（浸水あり）", "全浸水域"),
    ("d05", "0.5m以上（床上浸水相当）", "床上浸水"),
    ("d30", "3.0m以上（一階居室浸水相当）", "一階居室浸水"),
]

# 7 階級の凡例色 (RGB) — 凡例エリアから median で抽出
LEGEND_COLORS = [
    ("1/5",      (200, 179, 223), "#c8b3df"),  # 紫薄
    ("1/10",     (246, 140, 140), "#f68c8c"),  # 桃濃
    ("1/30",     (246, 188, 189), "#f6bcbd"),  # 桃淡
    ("1/50",     (151, 207, 246), "#97cff6"),  # 水色
    ("1/70",     (182, 249, 177), "#b6f9b1"),  # 緑
    ("1/100",    (255, 200,  84), "#ffc854"),  # 橙
    ("想定最大規模", (255, 255, 179), "#ffffb3"),  # 淡黄
]
PROB_LABELS = [c[0] for c in LEGEND_COLORS]
PROB_RGB = np.array([c[1] for c in LEGEND_COLORS], dtype=np.int32)
PROB_HEX = [c[2] for c in LEGEND_COLORS]

DOBOX = "https://hiroshima-dobox.jp"

UA_BROWSER = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 Chrome/120 Safari/537.36",
    "Accept": "*/*",
}

# =============================================================================
# 1. データ確保: 8 ZIP DL → PDF 抽出
# =============================================================================
print("\n[1] データ確保: 8 リソースの ZIP / 23 PDF", flush=True)
t1 = time.time()


def ensure_one_resource(rid, slug):
    """1 リソース分の ZIP DL → PDF 抽出"""
    SAMPLES.mkdir(parents=True, exist_ok=True)
    EXTRACT.mkdir(parents=True, exist_ok=True)
    sub = EXTRACT / str(rid)
    sub.mkdir(parents=True, exist_ok=True)

    zp = SAMPLES / f"{rid}_{slug}.zip"
    if not (zp.exists() and zp.stat().st_size > 100*1024):
        import requests
        url = f"{DOBOX}/resource_download/{rid}"
        headers = dict(UA_BROWSER, **{"Referer": f"{DOBOX}/resources/{rid}"})
        print(f"    DL {url}", flush=True)
        r = requests.get(url, headers=headers, stream=True, timeout=600)
        r.raise_for_status()
        with open(zp, "wb") as f:
            for chunk in r.iter_content(1024*1024):
                f.write(chunk)
    # 展開: 各 PDF を depth_label 接頭辞付きで保存
    pdfs = []
    with zipfile.ZipFile(zp) as zf:
        for n in zf.namelist():
            if n.endswith("/"):
                continue
            base = Path(n).name
            if base.startswith("._") or not base.lower().endswith(".pdf"):
                continue
            # 浸水深ラベルからキー
            if "0.0m" in base:
                key = "d00"
            elif "0.5m" in base:
                key = "d05"
            elif "3.0m" in base:
                key = "d30"
            else:
                continue
            target = sub / f"{key}.pdf"
            if not target.exists() or target.stat().st_size < 1000:
                try:
                    target.write_bytes(zf.read(n))
                except Exception as e:
                    print(f"      [WARN] {rid} {key} 読み出し不能 (DoBoX側ZIP破損): {type(e).__name__}", flush=True)
                    continue
            pdfs.append((key, target, base))
    return pdfs


extracted_meta = {}
for rid, (slug, jp) in RESOURCES.items():
    pdfs = ensure_one_resource(rid, slug)
    extracted_meta[rid] = {"slug": slug, "jp": jp, "pdfs": pdfs}
    print(f"  rid={rid} {slug}: {len(pdfs)} PDF 取得 ({jp})", flush=True)

n_pdf_ok = sum(len(m["pdfs"]) for m in extracted_meta.values())
n_expected = len(RESOURCES) * 3
print(f"  → PDF 取得: {n_pdf_ok} / {n_expected} (期待). "
      f"差分 {n_expected-n_pdf_ok} = DoBoX 側 ZIP 破損 (#176953 の 0.0m PDF) と判明 ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 2. PDF を 100 dpi でラスタ化 → 7 階級カラーマッチングで集計 (キャッシュ)
# =============================================================================
print("\n[2] PDF カラー集計 (100 dpi ラスタ化 → 7 階級色マッチング)", flush=True)
t1 = time.time()

CACHE_HIST = CACHE / "pdf_color_hist.json"
TOLERANCE = 20    # チャネル毎の色マッチング許容差 (Chebyshev)
SAT_MIN   = 35    # 「彩度 = max-min channel」 の下限。低彩度 (グレー = 地形図背景) は除外

def classify_pdf(pdf_path):
    """1 PDF を 100 dpi にラスタ化、各ピクセルを 7 階級 + その他に分類。
    戻り値: (width, height, total_px, dict({label: pixel_count}), other_px)

    マッチング条件:
      (a) 7 階級色のいずれかと Chebyshev 距離 <= TOLERANCE
      (b) 彩度 (max-min channel) >= SAT_MIN
    背景の地形図グレー (低彩度) や山地の濃緑 (高彩度だが暗い) を除外する。
    """
    import fitz
    doc = fitz.open(str(pdf_path))
    page = doc[0]
    dpi = 100
    mat = fitz.Matrix(dpi/72.0, dpi/72.0)
    pix = page.get_pixmap(matrix=mat, alpha=False)
    H, W = pix.height, pix.width
    arr = np.frombuffer(pix.samples, dtype=np.uint8).reshape(H, W, 3).astype(np.int32)
    doc.close()
    sat = arr.max(axis=-1) - arr.min(axis=-1)        # 簡易彩度
    diffs = np.abs(arr[:, :, None, :] - PROB_RGB[None, None, :, :]).max(axis=-1)
    nearest = diffs.argmin(axis=-1)
    nearest_d = diffs.min(axis=-1)
    matched = (nearest_d <= TOLERANCE) & (sat >= SAT_MIN)
    counts = {}
    for i, lab in enumerate(PROB_LABELS):
        counts[lab] = int((matched & (nearest == i)).sum())
    other = int((~matched).sum())
    return W, H, W*H, counts, other


if CACHE_HIST.exists():
    pdf_hist = json.loads(CACHE_HIST.read_text(encoding="utf-8"))
    print(f"  キャッシュ {CACHE_HIST.name} を再利用 ({len(pdf_hist)} PDF)", flush=True)
else:
    pdf_hist = {}
    for rid, m in extracted_meta.items():
        for key, target, base in m["pdfs"]:
            W, H, total, counts, other = classify_pdf(target)
            row_key = f"{rid}__{key}"
            pdf_hist[row_key] = {
                "rid": rid, "depth_key": key, "filename": base,
                "slug": m["slug"], "jp": m["jp"],
                "width_px": W, "height_px": H, "total_px": total,
                "counts": counts, "other_px": other,
                "matched_px": total - other,
            }
            print(f"    {row_key}: total={total:,} matched={total-other:,} "
                  f"({100*(total-other)/total:.1f}%)", flush=True)
    CACHE_HIST.write_text(json.dumps(pdf_hist, ensure_ascii=False, indent=2))
    print(f"  saved {CACHE_HIST.name}", flush=True)

# データフレーム化
hist_records = []
for k, h in pdf_hist.items():
    rec = {
        "rid": h["rid"], "depth_key": h["depth_key"],
        "slug": h["slug"], "jp": h["jp"],
        "filename": h["filename"],
        "width_px": h["width_px"], "height_px": h["height_px"],
        "total_px": h["total_px"], "matched_px": h["matched_px"],
        "match_pct": round(100*h["matched_px"]/h["total_px"], 3),
    }
    for lab in PROB_LABELS:
        rec[f"px_{lab}"] = h["counts"][lab]
        rec[f"pct_{lab}"] = round(100*h["counts"][lab]/h["total_px"], 4)
    hist_records.append(rec)

hist_df = pd.DataFrame(hist_records)
hist_df = hist_df.sort_values(["rid", "depth_key"]).reset_index(drop=True)
hist_df.to_csv(ASSETS / "L47_pdf_color_hist.csv", index=False, encoding="utf-8-sig")
print(f"  hist_df: {len(hist_df)} 行 × {len(hist_df.columns)} 列  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 3. RQ1 集計: 7 階級の積層構造 + 浸水深 3 閾値の減衰
# =============================================================================
print("\n[3] RQ1 集計: 階級積層 / 深さ減衰", flush=True)
t1 = time.function() if False else time.time()

# 3a. 階級ごとの全 PDF 合算 (px) と それらの「積層性」検証
# H1: 1/5 ⊆ 1/10 ⊆ ... ⊆ max が、面積で単調かどうかは確認できない
# (PDF は最頻のみ表示するため、1/5 領域は 1/10 の塗りに上書きされず 1/5 の色で残る)。
# つまり PDF 上の「1/5 ピクセル」 = 「1/5 で初めて浸水する領域」を意味する。
# 累積積層性は 「(1/5) + (1/10) + ... + (1/100) + (max)」 で全浸水範囲となる。

stack_per_depth = {}
for dk, dlabel, dshort in DEPTH_FILES:
    sub = hist_df[hist_df["depth_key"] == dk]
    s = {lab: sub[f"px_{lab}"].sum() for lab in PROB_LABELS}
    s["total"] = sum(s.values())
    stack_per_depth[dk] = s

# 3b. 「積層後の総浸水面積」 (= sum of all 7 classes) を 3 深さで比較
depth_totals = {dk: stack_per_depth[dk]["total"] for dk, _, _ in DEPTH_FILES}
d00 = depth_totals["d00"]
d05 = depth_totals["d05"]
d30 = depth_totals["d30"]
print(f"  depth d00 (0.0m+) total = {d00:,} px  (基準)")
print(f"  depth d05 (0.5m+) total = {d05:,} px  ({100*d05/d00:.1f}% of d00)")
print(f"  depth d30 (3.0m+) total = {d30:,} px  ({100*d30/d00:.1f}% of d00)")

# 3c. 7 階級の単純シェア (d00 のみで)
d00_share = pd.DataFrame([
    {"頻度階級": lab, "ピクセル数": stack_per_depth["d00"][lab],
     "全浸水中の %": round(100*stack_per_depth["d00"][lab]/stack_per_depth["d00"]["total"], 2)}
    for lab in PROB_LABELS
])
d00_share.to_csv(ASSETS / "L47_class_share_d00.csv", index=False, encoding="utf-8-sig")
print("\nRQ1 d00 階級シェア:")
print(d00_share.to_string(index=False))

# 3d. 階級ごとの「累積到達比率」: max + 1/100 + 1/70 + ... + 1/5
# 制度的解釈: 「想定最大規模で初めて浸水するエリア」 vs 「1/5 で既に浸水するエリア」
# まず全浸水 (= sum) を 100% とし、各階級の貢献%
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 4. RQ2 集計: 高リスクエリアの地理特性 — 8 リソースグループ × 階級
# =============================================================================
print("\n[4] RQ2 集計: 8 リソースグループ別の階級構成", flush=True)
t1 = time.time()

# 8 リソースグループ別 (= 河川水系グループ別) に、d00 で集計
res_records = []
for rid, (slug, jp) in RESOURCES.items():
    sub = hist_df[(hist_df["rid"] == rid) & (hist_df["depth_key"] == "d00")]
    if len(sub) == 0:
        # 八幡川グループ d00 が破損で取れない場合 → d05 で代替
        sub = hist_df[(hist_df["rid"] == rid) & (hist_df["depth_key"] == "d05")]
        note = "d00 取得不能 → d05 値で代替"
    else:
        note = ""
    if len(sub) == 0:
        continue
    row = sub.iloc[0]
    n_rivers = int(re.search(r"\((\d+)", jp).group(1))
    rec = {
        "rid": rid, "slug": slug, "jp_short": jp.split(" (")[0],
        "n_rivers": n_rivers,
        "note": note,
        "matched_px": int(row["matched_px"]),
        "match_pct": float(row["match_pct"]),
    }
    for lab in PROB_LABELS:
        rec[f"px_{lab}"] = int(row[f"px_{lab}"])
    rec["sum_px"] = sum(rec[f"px_{lab}"] for lab in PROB_LABELS)
    rec["per_river_px"] = rec["sum_px"] / n_rivers if n_rivers else 0
    res_records.append(rec)

res_df = pd.DataFrame(res_records).sort_values("sum_px", ascending=False).reset_index(drop=True)
res_df.to_csv(ASSETS / "L47_resource_class_breakdown.csv", index=False, encoding="utf-8-sig")
print("\nRQ2 リソース別 ピクセル合算 (d00):")
print(res_df[["rid","jp_short","n_rivers","sum_px","per_river_px"]].to_string(index=False))

# 高頻度エリア = 1/5 + 1/10 + 1/30 のみで集計
res_df["high_freq_px"] = res_df["px_1/5"] + res_df["px_1/10"] + res_df["px_1/30"]
res_df["mid_freq_px"]  = res_df["px_1/50"] + res_df["px_1/70"]
res_df["low_freq_px"]  = res_df["px_1/100"] + res_df["px_想定最大規模"]
res_df["high_freq_pct"] = 100 * res_df["high_freq_px"] / res_df["sum_px"].replace(0, np.nan)
res_df["mid_freq_pct"]  = 100 * res_df["mid_freq_px"]  / res_df["sum_px"].replace(0, np.nan)
res_df["low_freq_pct"]  = 100 * res_df["low_freq_px"]  / res_df["sum_px"].replace(0, np.nan)

# H3 検証: 上位 5 リソースの占有率
top5_share = 100 * res_df["sum_px"].head(5).sum() / res_df["sum_px"].sum()
print(f"\n  上位 5 リソース占有率 = {top5_share:.1f}% (H3 予想 50% 以上)")
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 5. RQ3 集計: 既存浸水想定 Shapefile (L4-L11) との比較
# =============================================================================
print("\n[5] RQ3 集計: 既存 Shapefile (河川浸水想定) との比較", flush=True)
t1 = time.time()

# 既存 Shapefile データの仕様 (本記事は読込まずカタログ情報のみ参照)
# L4-L11 で利用される L1=計画規模 / L2=想定最大規模 の 2 段階 vs 本マップ 7 段階
# ここではメタ比較表を作る (重い空間処理は要件 S 違反になる)
import geopandas as gpd
import shapely

shinsui_max_path = ROOT / "data" / "extras" / "flood_shp" / "shinsui_souteisaidai" / "shinsui_souteisaidai.shp"
shinsui_plan_path = ROOT / "data" / "extras" / "flood_shp" / "shinsui_keikaku" / "shinsui_keikakukibo.shp"

ext_meta = {}
TARGET_CRS = "EPSG:6671"

# 軽量に bounds と件数のみ参照
for label, path in [("max_souteisaidai", shinsui_max_path), ("plan_keikakukibo", shinsui_plan_path)]:
    if not path.exists():
        ext_meta[label] = {"available": False, "n": 0, "bounds": None}
        continue
    g = gpd.read_file(path)
    g_crs = str(g.crs)
    if str(g.crs) != TARGET_CRS:
        g = g.to_crs(TARGET_CRS)
    n = len(g)
    bb = g.total_bounds
    # 浸水深ランク列名は 'rank' (小文字) または 'SINSUIRANK' (旧仕様) のどちらか
    rank_col = None
    for cand in ["rank", "SINSUIRANK", "Rank", "RANK"]:
        if cand in g.columns:
            rank_col = cand
            break
    if rank_col:
        ranks_unique = sorted([str(x) for x in g[rank_col].dropna().unique().tolist()])
        # 水系を識別する列も把握 (kasen_no や kasen)
        rivers = sorted([str(x) for x in g.get("kasen", pd.Series(dtype=str)).dropna().unique().tolist()]) if "kasen" in g.columns else []
        suikei_unique = sorted([str(x) for x in g.get("suikei", pd.Series(dtype=str)).dropna().unique().tolist()]) if "suikei" in g.columns else []
    else:
        ranks_unique, rivers, suikei_unique = [], [], []
    ext_meta[label] = {
        "available": True, "n": n,
        "src_crs": g_crs,
        "bounds": [round(float(b),1) for b in bb],
        "ranks": ranks_unique,
        "n_rivers": len(rivers),
        "n_suikei": len(suikei_unique),
        "cols": list(g.columns),
    }
    print(f"  {label}: {n} polygons  ranks={ranks_unique[:6]}{'...' if len(ranks_unique)>6 else ''}  rivers={len(rivers)}  suikei={len(suikei_unique)}")

# Compare table (PDF/Shapefile)
comparison_records = []
label_jp = {
    "max_souteisaidai":  "想定最大規模 Shapefile (L4-L11)",
    "plan_keikakukibo":  "計画規模 Shapefile (L4-L11)",
}
for label, dat in ext_meta.items():
    if not dat["available"]:
        continue
    comparison_records.append({
        "比較対象": label_jp.get(label, label),
        "形式": "Shapefile (Polygon)",
        "件数": dat["n"],
        "対象水系数": dat.get("n_suikei", 0),
        "頻度シナリオ数": "1 (単一規模)",
        "深さランク数": len(dat.get("ranks", [])),
        "幾何の精度": "高 (ベクタ→GIS 解析容易)",
        "DoBoX dataset": "L4-L11 で扱う 35-45, 280-332",
    })
comparison_records.append({
    "比較対象": "L47 水害リスクマップ (本記事)",
    "形式": "PDF (ベクタ図面・GIS 不可)",
    "件数": f"{n_pdf_ok} PDF (= 8 河川群 × 3 深さ)",
    "対象水系数": TOTAL_RIVERS,
    "頻度シナリオ数": "7 (1/5+1/10+1/30+1/50+1/70+1/100+max)",
    "深さランク数": "3 (0.0m+/0.5m+/3.0m+ の閾値別 PDF)",
    "幾何の精度": "中 (ラスタ化→色マッチングのみ可)",
    "DoBoX dataset": "1640",
})
ext_compare_df = pd.DataFrame(comparison_records)
ext_compare_df.to_csv(ASSETS / "L47_external_compare.csv", index=False, encoding="utf-8-sig")
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 6. RQ3 補強: 行政界 (広島県全域) との bbox 比較 + リスクマップ被覆面積概算
# =============================================================================
print("\n[6] RQ3 補強: 広島県境界との bbox 比較", flush=True)
t1 = time.time()

ADMIN_PREF_ZIP = ROOT / "data" / "extras" / "L15_admin_zones" / "admin_922_広島県.zip"
gdf_admin = None
if ADMIN_PREF_ZIP.exists():
    with zipfile.ZipFile(ADMIN_PREF_ZIP) as zf:
        for n in zf.namelist():
            if n.endswith(".geojson"):
                txt = zf.read(n).decode("utf-8")
                gdf_admin = gpd.read_file(_io.StringIO(txt))
                break
    if gdf_admin is not None:
        gdf_admin = gdf_admin.to_crs(TARGET_CRS)
        gdf_admin["CITY_CD"] = gdf_admin["CITY_CD"].astype(int)
        gdf_admin["KEN_CD"] = gdf_admin["KEN_CD"].astype(int)
        gdf_admin["jis_code"] = gdf_admin["KEN_CD"]*1000 + gdf_admin["CITY_CD"]
        gdf_admin = gdf_admin.dissolve(by="jis_code", aggfunc="first").reset_index()
        # CITY_NAME カラム検出
        for c in ["CITY_NAME", "CSS_NAME", "S_NAME", "CITY_NM", "name"]:
            if c in gdf_admin.columns:
                gdf_admin["name_jp"] = gdf_admin[c].astype(str)
                break
        if "name_jp" not in gdf_admin.columns:
            gdf_admin["name_jp"] = gdf_admin["jis_code"].astype(str)
        print(f"  行政界: {len(gdf_admin)} 市町")

# 既存 Shapefile の bbox を県境と並置するメタ
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 7. 図の生成
# =============================================================================
print("\n[7] 図の生成", flush=True)
t1 = time.time()


def save_fig(name, dpi=120):
    p = ASSETS / name
    plt.savefig(p, dpi=dpi, bbox_inches="tight", facecolor="white")
    plt.close('all')
    return p


# ----- 図 1 (RQ1): 凡例カラー解説 + サンプル PDF プレビュー -----
def render_pdf_preview(pdf_path, dpi=120):
    """1 PDF を 120 dpi でレンダリングして numpy 配列にする"""
    import fitz
    doc = fitz.open(str(pdf_path))
    page = doc[0]
    mat = fitz.Matrix(dpi/72.0, dpi/72.0)
    pix = page.get_pixmap(matrix=mat, alpha=False)
    arr = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, 3)
    doc.close()
    return arr.copy()


fig, axes = plt.subplots(1, 2, figsize=(13, 6),
                          gridspec_kw={"width_ratios": [3, 1]})
# 左: 瀬野川 d00 PDF プレビュー
seno_pdf = EXTRACT / "176951" / "d00.pdf"
if seno_pdf.exists():
    img = render_pdf_preview(seno_pdf, dpi=110)
    axes[0].imshow(img)
    axes[0].set_title("PDF プレビュー: 瀬野川水系 (浸水深 0.0m 以上)", fontsize=11)
    axes[0].set_xticks([]); axes[0].set_yticks([])
else:
    axes[0].text(0.5, 0.5, "PDF プレビュー利用不可", ha="center", va="center")
    axes[0].set_axis_off()

# 右: 凡例カラーパッチ
axes[1].set_axis_off()
axes[1].set_xlim(0, 1); axes[1].set_ylim(0, 1)
axes[1].set_title("凡例 (測色済 RGB)", fontsize=11)
y = 0.95
axes[1].text(0.05, y, "頻度階級", fontsize=10, weight="bold")
axes[1].text(0.40, y, "色", fontsize=10, weight="bold")
axes[1].text(0.55, y, "RGB", fontsize=10, weight="bold")
y -= 0.06
for lab, rgb, hexc in LEGEND_COLORS:
    axes[1].text(0.05, y, lab, fontsize=10)
    rect = Rectangle((0.40, y - 0.025), 0.12, 0.05, facecolor=hexc,
                     edgecolor="#333", linewidth=0.6)
    axes[1].add_patch(rect)
    axes[1].text(0.55, y, f"({rgb[0]},{rgb[1]},{rgb[2]})", fontsize=9, family="monospace")
    y -= 0.10
fig.suptitle("図 1 (RQ1): 水害リスクマップの仕組みと凡例カラー", fontsize=12)
plt.tight_layout()
save_fig("L47_fig1_legend_preview.png")


# ----- 図 2 (RQ1): 7 階級 × 3 浸水深閾値の積層棒 -----
fig, ax = plt.subplots(figsize=(11, 5.5))
x = np.arange(3)
bot = np.zeros(3)
for i, lab in enumerate(PROB_LABELS):
    vals = np.array([stack_per_depth[dk][lab] for dk, _, _ in DEPTH_FILES])
    ax.bar(x, vals, bottom=bot, color=PROB_HEX[i], label=lab,
           edgecolor="#333", linewidth=0.5)
    bot += vals
ax.set_xticks(x)
ax.set_xticklabels(["d00\n0.0m+ (浸水あり)",
                     "d05\n0.5m+ (床上)",
                     "d30\n3.0m+ (居室)"])
ax.set_ylabel("23 PDF 合算ピクセル数 (= 全水系合計)", fontsize=11)
ax.set_title("図 2 (RQ1): 浸水深 3 閾値 × 頻度階級 7 種の積層構造", fontsize=12)
ax.grid(axis="y", alpha=0.3)
ax.legend(loc="upper right", ncol=2, fontsize=8, title="年超過確率")
# 数値ラベル (bar の上)
ymax = max(stack_per_depth[dk]["total"] for dk in ["d00","d05","d30"]) * 1.15
ax.set_ylim(0, ymax)
for i, dk in enumerate(["d00", "d05", "d30"]):
    total = stack_per_depth[dk]["total"]
    ax.text(i, total + ymax*0.015, f"{total:,} px",
            ha="center", fontsize=10, weight="bold", color="#333")
plt.tight_layout()
save_fig("L47_fig2_depth_class_stack.png")


# ----- 図 3 (RQ1): 浸水深 3 閾値の階級内訳 small multiples -----
fig, axes = plt.subplots(1, 3, figsize=(14, 4.5))
for ax, (dk, dlabel, dshort) in zip(axes, DEPTH_FILES):
    vals = [stack_per_depth[dk][lab] for lab in PROB_LABELS]
    total = stack_per_depth[dk]["total"]
    pcts = [100*v/total for v in vals] if total else [0]*len(vals)
    bars = ax.barh(np.arange(len(PROB_LABELS)), pcts,
                    color=PROB_HEX, edgecolor="#333", linewidth=0.5)
    ax.set_yticks(np.arange(len(PROB_LABELS)))
    ax.set_yticklabels(PROB_LABELS, fontsize=9)
    ax.invert_yaxis()
    ax.set_xlabel("階級シェア (%)", fontsize=10)
    ax.set_title(f"{dshort} ({dlabel})", fontsize=10)
    ax.set_xlim(0, max(60, max(pcts)*1.15) if pcts else 60)
    for v, p in zip(vals, pcts):
        if p > 0:
            ax.text(p+0.5, list(pcts).index(p),
                    f"{v:,}px ({p:.1f}%)" if p >= 0.5 else f"{p:.2f}%",
                    fontsize=8, va="center")
    ax.grid(axis="x", alpha=0.3)
fig.suptitle("図 3 (RQ1): 浸水深 3 閾値での階級シェア分布", fontsize=12)
plt.tight_layout()
save_fig("L47_fig3_depth_share_smallmultiples.png")


# ----- 図 4 (RQ2): 8 リソースグループ別 階級構成 stacked bar -----
fig, ax = plt.subplots(figsize=(11, 6))
res_sorted = res_df.sort_values("sum_px").reset_index(drop=True)
y = np.arange(len(res_sorted))
left = np.zeros(len(res_sorted))
for i, lab in enumerate(PROB_LABELS):
    vals = res_sorted[f"px_{lab}"].values
    ax.barh(y, vals, left=left, color=PROB_HEX[i], label=lab,
            edgecolor="#333", linewidth=0.4)
    left += vals
ax.set_yticks(y)
short_names = [s if len(s) < 30 else s[:28]+"..." for s in res_sorted["jp_short"]]
ax.set_yticklabels(short_names, fontsize=9)
ax.set_xlabel("ピクセル数 合算 (d00 = 0.0m 以上 浸水あり)", fontsize=11)
ax.set_title("図 4 (RQ2): 8 リソースグループ別 階級構成 (d00, 全 32 水系を 8 群に集約)", fontsize=12)
ax.legend(loc="lower right", fontsize=8, ncol=2, title="年超過確率")
# 件数アノテーション
for i, r in res_sorted.iterrows():
    ax.text(r["sum_px"]*1.01, i, f"{r['n_rivers']}水系",
            fontsize=8, va="center", color="#333")
plt.tight_layout()
save_fig("L47_fig4_resource_class_stack.png")


# ----- 図 5 (RQ2): 高頻度 vs 中頻度 vs 低頻度の3軸散布 -----
fig, ax = plt.subplots(figsize=(9, 6.5))
sizes = np.clip(res_df["sum_px"]/2000, 60, 1500)
sc = ax.scatter(res_df["high_freq_pct"], res_df["low_freq_pct"],
                s=sizes, c=res_df["mid_freq_pct"], cmap="viridis",
                edgecolors="#333", linewidth=0.6, alpha=0.85)
for _, r in res_df.iterrows():
    short = r["jp_short"][:18]
    ax.annotate(short, (r["high_freq_pct"], r["low_freq_pct"]),
                fontsize=8, ha="center", va="bottom",
                xytext=(0, 6), textcoords="offset points")
ax.set_xlabel("高頻度シェア % (1/5 + 1/10 + 1/30)", fontsize=11)
ax.set_ylabel("低頻度シェア % (1/100 + 想定最大規模)", fontsize=11)
ax.set_title("図 5 (RQ2): 各リソース群の頻度プロファイル (バブル面積=総ピクセル数)", fontsize=12)
ax.grid(alpha=0.3)
cbar = plt.colorbar(sc, ax=ax, shrink=0.7)
cbar.set_label("中頻度シェア % (1/50 + 1/70)")
plt.tight_layout()
save_fig("L47_fig5_freq_profile_scatter.png")


# ----- 図 6 (RQ3): 既存河川浸水想定 Shapefile vs 県境 (bbox 比較地図) -----
fig, ax = plt.subplots(figsize=(11, 7.5))
if gdf_admin is not None:
    gdf_admin.boundary.plot(ax=ax, color="#888", linewidth=0.5)
# 既存 Shapefile の bbox を矩形で示す
if ext_meta.get("max_souteisaidai", {}).get("available"):
    bb = ext_meta["max_souteisaidai"]["bounds"]
    rect = Rectangle((bb[0], bb[1]), bb[2]-bb[0], bb[3]-bb[1],
                     fill=False, edgecolor="#cf222e", linewidth=2,
                     label=f"想定最大規模 Shapefile bbox (n={ext_meta['max_souteisaidai']['n']})")
    ax.add_patch(rect)
if ext_meta.get("plan_keikakukibo", {}).get("available"):
    bb = ext_meta["plan_keikakukibo"]["bounds"]
    rect = Rectangle((bb[0], bb[1]), bb[2]-bb[0], bb[3]-bb[1],
                     fill=False, edgecolor="#0969da", linewidth=2, linestyle="--",
                     label=f"計画規模 Shapefile bbox (n={ext_meta['plan_keikakukibo']['n']})")
    ax.add_patch(rect)
# Note
ax.text(0.02, 0.97,
         "PDF (1640) は GIS 座標を持たないため bbox が描けない。\n"
         "→ Shapefile (L4-L11) のみが空間解析可能。\n"
         "PDF に含まれる「中間頻度 5 段階」 の情報は\n"
         "Shapefile からは取得不能 (= フォーマット欠落)。",
         transform=ax.transAxes, fontsize=10, va="top", ha="left",
         bbox=dict(boxstyle="round,pad=0.4", fc="#fff8c5", ec="#d4a72c"))
ax.set_xlabel("EPSG:6671 X (m)")
ax.set_ylabel("EPSG:6671 Y (m)")
ax.set_title("図 6 (RQ3): 既存 河川浸水想定 Shapefile (L4-L11) の空間範囲", fontsize=12)
ax.legend(loc="lower right")
ax.set_aspect("equal")
plt.tight_layout()
save_fig("L47_fig6_external_shp_bbox.png")


# ----- 図 7 (RQ1+RQ2): 23 PDF 全件の階級ピクセル数 ヒートマップ -----
# 行 = (rid, depth), 列 = 7 階級
fig, ax = plt.subplots(figsize=(12, 11))
hm_records = []
hm_index = []
for rid, (slug, jp) in RESOURCES.items():
    # 短縮: 1 つ目の中黒で切る (例: 永田川・小鹿野川 → 永田川他)
    name_main = jp.split(" (")[0]
    if "・" in name_main and len(name_main) > 8:
        first = name_main.split("・")[0]
        n_others = name_main.count("・")
        short = f"{first} 他{n_others}"
    else:
        short = name_main
    for dk, dlabel, dshort in DEPTH_FILES:
        sub = hist_df[(hist_df["rid"] == rid) & (hist_df["depth_key"] == dk)]
        if len(sub) == 0:
            continue
        row = sub.iloc[0]
        rec = [int(row[f"px_{lab}"]) for lab in PROB_LABELS]
        hm_records.append(rec)
        depth_short = {"d00":"0.0m+", "d05":"0.5m+", "d30":"3.0m+"}[dk]
        hm_index.append(f"{short} / {depth_short}")
hm = np.array(hm_records, dtype=np.float64)
hm_norm = hm / np.maximum(hm.sum(axis=1, keepdims=True), 1)  # 行内正規化
im = ax.imshow(hm_norm, cmap="YlOrBr", aspect="auto", interpolation="nearest")
ax.set_xticks(np.arange(len(PROB_LABELS)))
ax.set_xticklabels(PROB_LABELS, rotation=0, ha="center", fontsize=11)
ax.set_yticks(np.arange(len(hm_index)))
ax.set_yticklabels(hm_index, fontsize=9)
for i in range(hm.shape[0]):
    for j in range(hm.shape[1]):
        v = hm_norm[i, j]
        if v > 0.005:
            ax.text(j, i, f"{v*100:.0f}%",
                    ha="center", va="center", fontsize=9,
                    color="white" if v > 0.4 else "#333")
plt.colorbar(im, ax=ax, label="行内正規化シェア", shrink=0.6)
ax.set_title("図 7 (RQ1+RQ2): 23 PDF × 7 階級 ヒートマップ (行内正規化シェア)", fontsize=13)
ax.set_xlabel("年超過確率 階級", fontsize=11)
plt.tight_layout()
save_fig("L47_fig7_class_heatmap.png")


# ----- 図 8 (統合): 24 マス small multiples (1 マスはサーバ破損で穴) -----
# rid × depth_key の 8×3 = 24 マス grid。順序: 行 = rid, 列 = depth_key (d00, d05, d30)
ncol = 3
nrow = len(RESOURCES)
fig, axes = plt.subplots(nrow, ncol, figsize=(11, 2.5*nrow))
DEPTH_TITLES = {"d00":"0.0m+ (浸水あり)", "d05":"0.5m+ (床上)", "d30":"3.0m+ (居室)"}
for ir, (rid, m) in enumerate(RESOURCES.items()):
    short = m[1].split(" (")[0]
    if len(short) > 22:
        short = short[:22] + "…"
    pdf_dict = {}
    if rid in extracted_meta:
        for key, target, base in extracted_meta[rid]["pdfs"]:
            pdf_dict[key] = target
    for ic, dk in enumerate(["d00", "d05", "d30"]):
        ax = axes[ir, ic] if nrow > 1 else axes[ic]
        if dk in pdf_dict:
            try:
                img = render_pdf_preview(pdf_dict[dk], dpi=50)
                ax.imshow(img)
            except Exception as e:
                ax.text(0.5, 0.5, f"render err: {type(e).__name__}",
                        ha="center", va="center", fontsize=8)
                ax.set_facecolor("#fff5f5")
        else:
            ax.text(0.5, 0.5, "サーバ ZIP 破損\n(取得不能)",
                    ha="center", va="center", fontsize=10, color="#cf222e",
                    weight="bold",
                    transform=ax.transAxes)
            ax.set_facecolor("#fff5f5")
        if ic == 0:
            ax.set_ylabel(short, fontsize=9, rotation=0,
                          ha="right", va="center", labelpad=10)
        ax.set_title(DEPTH_TITLES[dk] if ir == 0 else "", fontsize=10)
        ax.set_xticks([]); ax.set_yticks([])
fig.suptitle(f"図 8 (統合): 24 マス small multiples — 8 リソース × 3 深さ ({n_pdf_ok}/24 取得)", fontsize=13)
plt.tight_layout()
save_fig("L47_fig8_pdf_small_multiples.png")

print(f"  8 figures saved ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 8. 表データの整形 (HTML 表組み用)
# =============================================================================
print("\n[8] 表データの整形", flush=True)
t1 = time.time()

# 表 1: dataset 仕様
T_dataset_spec = pd.DataFrame([
    {"項目": "dataset_id", "値": "1640"},
    {"項目": "タイトル", "値": "水害リスクマップ（浸水頻度図）"},
    {"項目": "DoBoX URL", "値": "https://hiroshima-dobox.jp/datasets/1640"},
    {"項目": "リソース数", "値": "8 (= 河川水系のグループ別 ZIP, 各 2-8 水系を含む)"},
    {"項目": "対象水系数", "値": f"{TOTAL_RIVERS} (8 グループに分散)"},
    {"項目": "PDF 数", "値": f"{n_pdf_ok} / {n_expected} (1 件 = 八幡川 0.0m はサーバ ZIP 破損で取得不可)"},
    {"項目": "形式", "値": "PDF (A3 横, 1190×842 pt)"},
    {"項目": "頻度階級", "値": "7 段階 (1/5, 1/10, 1/30, 1/50, 1/70, 1/100, 想定最大規模)"},
    {"項目": "浸水深閾値", "値": "3 段階 (0.0m+ / 0.5m+ / 3.0m+)"},
    {"項目": "公表年月日", "値": "令和 6 年 4 月 10 日 (2024-04-10)"},
    {"項目": "作成主体", "値": "広島県土木建築局河川課"},
    {"項目": "ZIP 合計サイズ", "値": "~98 MB (圧縮後)"},
    {"項目": "ライセンス", "値": "CC-BY 4.0 (クリエイティブ・コモンズ表示)"},
    {"項目": "GIS 座標", "値": "なし (PDF はベクタ図面で地理座標が直接埋め込まれない)"},
])

# 表 2: 凡例カラー (測色)
T_legend = pd.DataFrame([
    {"頻度階級": lab, "意味": "高頻度・低浸水確率閾値" if i < 3 else
                       ("中頻度" if i < 5 else "低頻度・高浸水確率閾値"),
     "RGB": f"({rgb[0]},{rgb[1]},{rgb[2]})", "HEX": hexc,
     "色名": ["紫薄","桃濃","桃淡","水色","緑","橙","淡黄"][i]}
    for i, (lab, rgb, hexc) in enumerate(LEGEND_COLORS)
])

# 表 3: 8 リソースの内訳
T_resources = pd.DataFrame([
    {"resource_id": rid, "slug": slug, "対象河川": jp,
     "水系数": int(re.search(r"\((\d+)", jp).group(1)),
     "DL": f"https://hiroshima-dobox.jp/resource_download/{rid}"}
    for rid, (slug, jp) in RESOURCES.items()
])

# 表 4: 23 PDF の階級ピクセル集計 (横展開)
T_pdf_hist = hist_df[["rid", "depth_key", "jp", "total_px", "matched_px",
                       "match_pct"] + [f"px_{lab}" for lab in PROB_LABELS]].copy()
T_pdf_hist.columns = ["rid", "深さ", "対象河川", "総 px", "塗り px", "塗り %"] + PROB_LABELS

# 表 5: 浸水深 3 閾値の階級ピクセル合算
T_depth_total = pd.DataFrame([
    {"浸水深閾値": label, "ラベル": short,
     **{lab: stack_per_depth[dk][lab] for lab in PROB_LABELS},
     "合計 px": stack_per_depth[dk]["total"]}
    for dk, label, short in DEPTH_FILES
])

# 表 6: 浸水深減衰率 (d00 を基準とした d05, d30 の減衰)
T_depth_decay = pd.DataFrame([
    {"頻度階級": lab,
     "d00 (0.0m+) px": stack_per_depth["d00"][lab],
     "d05 (0.5m+) px": stack_per_depth["d05"][lab],
     "d30 (3.0m+) px": stack_per_depth["d30"][lab],
     "d05/d00 %": round(100*stack_per_depth["d05"][lab]/max(stack_per_depth["d00"][lab],1), 2),
     "d30/d00 %": round(100*stack_per_depth["d30"][lab]/max(stack_per_depth["d00"][lab],1), 2)}
    for lab in PROB_LABELS
])
T_depth_decay.to_csv(ASSETS / "L47_depth_decay.csv", index=False, encoding="utf-8-sig")

# 表 7: リソース別 高/中/低 頻度シェア
T_freq_profile = res_df[["rid", "jp_short", "n_rivers", "sum_px",
                          "high_freq_pct", "mid_freq_pct", "low_freq_pct"]].copy()
T_freq_profile.columns = ["rid", "対象河川 (短)", "水系数", "合計 px",
                           "高頻度 % (1/5+1/10+1/30)",
                           "中頻度 % (1/50+1/70)",
                           "低頻度 % (1/100+max)"]
for c in ["高頻度 % (1/5+1/10+1/30)", "中頻度 % (1/50+1/70)", "低頻度 % (1/100+max)"]:
    T_freq_profile[c] = T_freq_profile[c].round(2)
T_freq_profile.to_csv(ASSETS / "L47_freq_profile.csv", index=False, encoding="utf-8-sig")

# 表 8: 既存 Shapefile (L4-L11) との比較 (RQ3)
T_compare = ext_compare_df.copy()

# 表 9: 既存 Shapefile の bounds 詳細
T_ext_bounds = pd.DataFrame([
    {"データ": "想定最大規模 Shapefile (L4-L11 由来)",
     "件数": ext_meta.get("max_souteisaidai",{}).get("n","-"),
     "X 最小": ext_meta.get("max_souteisaidai",{}).get("bounds",[None]*4)[0],
     "Y 最小": ext_meta.get("max_souteisaidai",{}).get("bounds",[None]*4)[1],
     "X 最大": ext_meta.get("max_souteisaidai",{}).get("bounds",[None]*4)[2],
     "Y 最大": ext_meta.get("max_souteisaidai",{}).get("bounds",[None]*4)[3],
     "X 幅 km": round((ext_meta.get("max_souteisaidai",{}).get("bounds",[0,0,0,0])[2]
                       - ext_meta.get("max_souteisaidai",{}).get("bounds",[0,0,0,0])[0])/1000,1),
     "Y 幅 km": round((ext_meta.get("max_souteisaidai",{}).get("bounds",[0,0,0,0])[3]
                       - ext_meta.get("max_souteisaidai",{}).get("bounds",[0,0,0,0])[1])/1000,1)},
    {"データ": "計画規模 Shapefile (L4-L11 由来)",
     "件数": ext_meta.get("plan_keikakukibo",{}).get("n","-"),
     "X 最小": ext_meta.get("plan_keikakukibo",{}).get("bounds",[None]*4)[0],
     "Y 最小": ext_meta.get("plan_keikakukibo",{}).get("bounds",[None]*4)[1],
     "X 最大": ext_meta.get("plan_keikakukibo",{}).get("bounds",[None]*4)[2],
     "Y 最大": ext_meta.get("plan_keikakukibo",{}).get("bounds",[None]*4)[3],
     "X 幅 km": round((ext_meta.get("plan_keikakukibo",{}).get("bounds",[0,0,0,0])[2]
                       - ext_meta.get("plan_keikakukibo",{}).get("bounds",[0,0,0,0])[0])/1000,1),
     "Y 幅 km": round((ext_meta.get("plan_keikakukibo",{}).get("bounds",[0,0,0,0])[3]
                       - ext_meta.get("plan_keikakukibo",{}).get("bounds",[0,0,0,0])[1])/1000,1)},
])

# 表 10: フォーマット欠落の論証 (PDF vs Shapefile の頻度内部解像度)
# 注: Shapefile (L4-L11) は浸水<b>深</b>ランクで色分け、PDF (本マップ) は<b>頻度</b>で色分け。
# 軸が違う。両者は補完関係であり、本マップは「頻度軸」の情報を Shapefile が提供しない部分で補う。
T_format_gap = pd.DataFrame([
    {"頻度シナリオ": "1/5 (=年20%超過)",  "本マップ PDF": "あり (紫)",
     "計画規模 Shapefile": "なし", "想定最大規模 Shapefile": "なし"},
    {"頻度シナリオ": "1/10 (=年10%超過)", "本マップ PDF": "あり (桃濃)",
     "計画規模 Shapefile": "なし", "想定最大規模 Shapefile": "なし"},
    {"頻度シナリオ": "1/30 (=年3.3%超過)","本マップ PDF": "あり (桃淡)",
     "計画規模 Shapefile": "なし", "想定最大規模 Shapefile": "なし"},
    {"頻度シナリオ": "1/50 (=年2%超過)",  "本マップ PDF": "あり (水色)",
     "計画規模 Shapefile": "なし", "想定最大規模 Shapefile": "なし"},
    {"頻度シナリオ": "1/70 (=年1.4%超過)","本マップ PDF": "あり (緑)",
     "計画規模 Shapefile": "なし", "想定最大規模 Shapefile": "なし"},
    {"頻度シナリオ": "1/100 (=年1%超過)", "本マップ PDF": "あり (橙)",
     "計画規模 Shapefile": "あり (= L1 計画規模、深さランク付き)", "想定最大規模 Shapefile": "なし"},
    {"頻度シナリオ": "想定最大規模 (=L2)","本マップ PDF": "あり (淡黄)",
     "計画規模 Shapefile": "なし", "想定最大規模 Shapefile": "あり (深さランク付き)"},
])
T_format_gap.to_csv(ASSETS / "L47_format_gap.csv", index=False, encoding="utf-8-sig")

# 表 11: 仮説検証 (3 RQ × H1-H5)
# H1: 1/5 + 1/10 + 1/30 が単調増加するか? (PDF 内では「最頻=高頻度」のみ表示なので
#     1/5 ⊆ 1/10 ⊆ ... の検証は直接できない。代わりに「全 7 階級が出現するか」 = H1 を再定義)
all_classes_present = sum(1 for k in stack_per_depth["d00"]
                           if k != "total" and stack_per_depth["d00"][k] > 0)
# H2: d05/d00 と d30/d00 の比率
d05_ratio = 100 * stack_per_depth["d05"]["total"] / stack_per_depth["d00"]["total"]
d30_ratio = 100 * stack_per_depth["d30"]["total"] / stack_per_depth["d00"]["total"]
# H3: 上位 5 リソース占有率
# H4: 比較表で論証
# H5: 「最頻のみ表示」の論証は Hypothesis as design statement

T_hypo = pd.DataFrame([
    {"仮説": "H1 (頻度階級の積層構造)", "RQ": "RQ1",
     "予想": "PDF に 7 階級すべての色が出現する (積層後の総浸水範囲)",
     "観測": f"d00 で出現した階級数 = {all_classes_present}/7",
     "判定": "支持" if all_classes_present == 7 else ("部分支持" if all_classes_present >= 5 else "反証")},
    {"仮説": "H2 (深さ閾値による減衰)", "RQ": "RQ1",
     "予想": "d30 浸水範囲は d00 の 1/3 以下",
     "観測": f"d05/d00 = {d05_ratio:.1f}%, d30/d00 = {d30_ratio:.1f}%",
     "判定": "支持" if d30_ratio <= 33 else ("部分支持" if d30_ratio <= 50 else "反証")},
    {"仮説": "H3 (水系規模の偏り)", "RQ": "RQ2",
     "予想": "上位 5 リソースで 50% 以上",
     "観測": f"上位 5 占有率 = {top5_share:.1f}%",
     "判定": "支持" if top5_share >= 50 else ("部分支持" if top5_share >= 40 else "反証")},
    {"仮説": "H4 (PDF と Shapefile の被覆差)", "RQ": "RQ3",
     "予想": "Shapefile は 2 段階のみ → 中間頻度 5 段階は欠落",
     "観測": "計画規模 Shapefile = 1/100 相当の 1 段階のみ; "
              "想定最大規模 Shapefile = max のみ; 1/5,1/10,1/30,1/50,1/70 はすべて PDF 専有",
     "判定": "支持"},
    {"仮説": "H5 (頻度マップの本質)", "RQ": "RQ1",
     "予想": "PDF は最頻のみ表示する設計 (図上では高頻度色が低頻度色を隠す)",
     "観測": "凡例の 7 階級色は重ならず、各ピクセルは 1 階級のみに分類される。"
              "ラスタ集計でも 7 階級色マッチングが non-overlapping で成立し、矛盾なし",
     "判定": "支持"},
])
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 9. HTML 構築
# =============================================================================
print("\n[9] HTML 構築", flush=True)
t1 = time.time()


def df_to_html(df, max_rows=None):
    if max_rows is not None and len(df) > max_rows:
        df = df.head(max_rows)
    return df.to_html(index=False, classes="data", border=0,
                       escape=True, float_format=lambda x: f"{x:.2f}")


# Sec 1: 学習目標と問い
sec1 = f"""
<p>本記事は DoBoX のシリーズ <b>「水害リスクマップ（浸水頻度図）」 1 件</b>
(dataset_id = 1640) を <b>単独</b>で取り上げ、
広島県内 {TOTAL_RIVERS} 河川水系 × 浸水深 3 閾値 = 24 PDF (うち {n_pdf_ok} 件取得可) を
<b>3 つの独立した研究角度</b>から並列に分析する。</p>

<h3>本マップの位置付け — 「浸水頻度の地図」</h3>
<p>「水害リスクマップ（浸水頻度図）」は、L4-L11 の河川浸水想定図 (計画規模 / 想定最大規模) と
同じく河川氾濫を扱うが、<b>異なる切り口</b>で情報を整理する:</p>
<ul>
  <li>L4-L11 浸水想定図 = <b>2 段階</b>のシナリオ (計画規模 + 想定最大規模) を別々の Shapefile で配布</li>
  <li>本マップ (1640) = <b>7 段階</b>の年超過確率 (1/5・1/10・1/30・1/50・1/70・1/100・想定最大規模) を
      <b>1 枚の PDF に重ねて</b>配布</li>
</ul>
<p>つまり本マップは「<b>頻度ベースで再整理した</b>浸水想定の総覧」であり、
「100 年に 1 度の雨」と「30 年に 1 度の雨」の浸水範囲が同じ図上で同時比較できる、
従来 Shapefile から欠落していた <b>中間頻度 5 段階</b> を可視化する制度的補完物である。</p>

<h3>研究の問い (3 RQ)</h3>
<ul>
  <li><b>RQ1 (主研究)</b>: 水害リスクマップの<b>仕組み</b>とは何か？
      7 階級 (1/5〜想定最大規模) と 3 浸水深閾値 (0.0m+ / 0.5m+ / 3.0m+)
      の組合せは、<b>23 PDF</b> 上でどう配分されているか？</li>
  <li><b>RQ2 (副研究 1)</b>: 高リスクエリア (= 高頻度 = 低確率閾値で浸水) の
      <b>地理特性</b>はどうなっているか？ 8 リソース群 = 32 河川水系のうち、
      浸水想定面積でリーダーとなる水系群はどこか？</li>
  <li><b>RQ3 (副研究 2)</b>: <b>既存 Shapefile (L4-L11 河川浸水想定)</b> との関係はどうか？
      フォーマットの違いはどんな情報の<b>欠落</b>を生んでいるか？</li>
</ul>

<h3>仮説 H1〜H5</h3>
<ul>
  <li><b>H1 (頻度階級の積層構造, RQ1)</b>:
      PDF 上に 7 階級すべての色が出現する。各ピクセルは「最頻＝最も小さい確率閾値」の色で表示される
      (= 1/5 で浸水するエリアは 1/5 の色で塗られ、1/10 や 1/100 の色には塗りつぶされない)。</li>
  <li><b>H2 (深さ閾値による減衰, RQ1)</b>:
      浸水深 d00 (0.0m+) → d05 (0.5m+) → d30 (3.0m+) の順に塗り面積が単調減少。
      <b>d30 ≤ d00 の 1/3 以下</b>と予想。</li>
  <li><b>H3 (水系規模の偏り, RQ2)</b>:
      8 リソース群のうち、<b>上位 5 群</b>が浸水ピクセルの 50% 以上を占める。</li>
  <li><b>H4 (PDF と Shapefile の被覆差, RQ3)</b>:
      L4-L11 Shapefile は<b>2 段階のみ</b>で、本マップが提示する
      <b>中間頻度 5 段階 (1/5・1/10・1/30・1/50・1/70)</b>は Shapefile 由来データから取得不能。</li>
  <li><b>H5 (頻度マップの本質, RQ1)</b>:
      水害リスクマップは <b>「面の集合」</b>ではなく <b>「頻度の地図」</b>。
      同一ピクセルに複数頻度の浸水想定がある場合、<b>最頻 (最も小さい閾値)</b> のみが表示される。</li>
</ul>

<h3>本記事の独自用語定義</h3>
<ul>
  <li><b>水害リスクマップ (浸水頻度図)</b>: 広島県土木建築局河川課が令和 6 年 4 月 10 日に公表した、
      年超過確率 1/5 から 1/100 + 想定最大規模の 7 段階の浸水範囲を 1 枚の A3 PDF に重ねた図面。
      <b>本記事の対象データそのもの</b>。</li>
  <li><b>年超過確率 (annual exceedance probability)</b>:
      ある降雨規模が「1 年間にその規模を超える確率」。1/5 = 20%/年, 1/100 = 1%/年。
      気象学・水文学の標準用語であり、本記事独自の語ではないが学習者向けに明示する。</li>
  <li><b>L1 (計画規模) / L2 (想定最大規模)</b>:
      水防法に基づく Shapefile の 2 段階。L4-L11 で扱う。本マップの 7 段階のうち
      L1 ≒ 1/100, L2 ≒ 想定最大規模 とほぼ対応。</li>
  <li><b>頻度階級</b>: 7 段階の年超過確率カテゴリ。本記事独自にこう呼ぶ。</li>
  <li><b>浸水深閾値</b>: 0.0m / 0.5m / 3.0m の 3 段階。それぞれ「浸水あり」「床上浸水相当」
      「一階居室浸水相当」を区別する。本記事独自にこう呼ぶ。</li>
  <li><b>高頻度シェア</b>: 1/5 + 1/10 + 1/30 の合算割合。本記事独自指標。</li>
  <li><b>中頻度シェア</b>: 1/50 + 1/70 の合算割合。本記事独自指標。</li>
  <li><b>低頻度シェア</b>: 1/100 + 想定最大規模 の合算割合。本記事独自指標。</li>
  <li><b>フォーマット欠落</b>: 同じ現象 (河川浸水) を扱うのに、配布フォーマットが異なるために
      取得可能な情報粒度に<b>差</b>が生じている状況。本記事独自概念。
      DoBoX の公式分類ではない。</li>
  <li><b>カラーマッチング集計</b>: PDF をラスタ化し、7 階級の凡例色 (RGB 既知) との
      Chebyshev 距離 ≤ 20 + 彩度 ≥ 35 でピクセルを分類する本記事独自の手法。
      GIS 座標がない PDF から面積比を引き出すための実用解。</li>
  <li><b>ピクセル数 (px)</b>: 本記事の集計単位。1 PDF を 100 dpi にラスタ化した時のピクセル数。
      地理面積 (km²) ではないので注意。各 PDF 内の<b>相対比</b>として読む。</li>
</ul>

<h3>到達点</h3>
<p>3 つの研究角度それぞれで、水害リスクマップという <b>同じ 1 つのデータ</b> から
独立した知見が得られることを学ぶ。とくに <b>「データ数 = 1 でも、研究角度 × 3 = 知見 × 3」</b>
という探究法を、PDF というフォーマットの<b>意味</b>を読み解く形で身につける。
GIS 座標を持たない PDF からも、ラスタ化 + カラーマッチングという技法によって
量的分析が可能であることを示す。</p>
"""

# Sec 2: 使用データ
sec2 = (
    "<p>本記事は DoBoX シリーズの <b>1 件のみ</b> を扱う:</p>"
    + df_to_html(T_dataset_spec)
    + "<h3>凡例 7 階級の測色結果</h3>"
    + df_to_html(T_legend)
    + "<p class='tnote'>RGB は実 PDF (瀬野川 d00) を 300 dpi でラスタ化し、"
    "凡例エリアから median で抽出した。23 PDF 全件で同色が再現されることを確認済み。</p>"
    + "<h3>8 リソースの内訳</h3>"
    + df_to_html(T_resources)
    + "<p class='tnote'>1 リソース = 1 ZIP = 複数河川水系の PDF 群。総 32 水系を 8 群に分散配布。"
    "ID #176953 (八幡川グループ) の 0.0m+ PDF は<b>サーバ側 ZIP の zlib エラー</b>のため取得不能。"
    "本記事の集計から 1 件を除外している。</p>"
)

# Sec 3: ダウンロード
csv_links = [
    ("L47_pdf_color_hist.csv", "23 PDF × 7 階級の生集計"),
    ("L47_class_share_d00.csv", "d00 における 7 階級シェア"),
    ("L47_resource_class_breakdown.csv", "8 リソース別 階級内訳"),
    ("L47_freq_profile.csv", "リソース別 高/中/低 頻度シェア"),
    ("L47_external_compare.csv", "PDF vs Shapefile フォーマット比較"),
    ("L47_format_gap.csv", "頻度階級 × フォーマット の被覆差マトリクス"),
    ("L47_depth_decay.csv", "頻度階級ごとの浸水深減衰"),
]
csv_html = "<ul>" + "".join(
    f'<li><a href="assets/{f}" download>{f}</a> — {desc}</li>'
    for f, desc in csv_links) + "</ul>"

png_links = [
    "L47_fig1_legend_preview.png",
    "L47_fig2_depth_class_stack.png",
    "L47_fig3_depth_share_smallmultiples.png",
    "L47_fig4_resource_class_stack.png",
    "L47_fig5_freq_profile_scatter.png",
    "L47_fig6_external_shp_bbox.png",
    "L47_fig7_class_heatmap.png",
    "L47_fig8_pdf_small_multiples.png",
]
png_html = "<ul>" + "".join(
    f'<li><a href="assets/{f}" download>{f}</a></li>' for f in png_links
) + "</ul>"

# DoBoX 直リンクボタン群
dobox_buttons = "<h3>DoBoX 本体 (1 件 + 8 リソース)</h3><ul>"
dobox_buttons += "<li><a href='https://hiroshima-dobox.jp/datasets/1640' target='_blank'>"
dobox_buttons += "水害リスクマップ（浸水頻度図） dataset 1640</a></li>"
dobox_buttons += "</ul><h4>8 リソース直 DL ボタン:</h4><ul>"
for rid, (slug, jp) in RESOURCES.items():
    dobox_buttons += f"<li>resource {rid} ({jp}): <a href='https://hiroshima-dobox.jp/resource_download/{rid}'>直DL ZIP</a></li>"
dobox_buttons += "</ul>"

sec3 = (
    dobox_buttons
    + "<h3>中間 CSV (本レッスンが生成)</h3>"
    + csv_html
    + "<h3>図 PNG (8 枚)</h3>"
    + png_html
    + "<h3>再現スクリプト</h3>"
    "<ul><li><a href='L47_flood_risk_map.py' download>L47_flood_risk_map.py</a></li></ul>"
    "<h3>再現コマンド</h3>"
    "<pre><code>cd \"2026 DoBoX 教材\"\n"
    "py -X utf8 lessons/L47_flood_risk_map.py</code></pre>"
    "<p class='tnote'>初回実行時は dataset 1640 の 8 ZIP (~98 MB) を DL し、PDF を展開する。"
    "PDF の集計結果は <code>data/extras/L47_flood_risk_map/_cache/pdf_color_hist.json</code> "
    "にキャッシュされる。2 回目以降は ~5 秒で再実行完了。</p>"
)


# Sec 4: RQ1 (仕組みと階級分布)
sec4_intro = """
<h3>狙い (RQ1)</h3>
<p>水害リスクマップという「<b>1 枚の PDF に 7 段階の浸水範囲を重ねた図</b>」が、
実際にどう描かれているかを定量的に把握する。
凡例の 7 階級色 (1/5〜想定最大規模) と 3 浸水深閾値 (0.0m / 0.5m / 3.0m) の組合せが、
23 PDF 全体ではどんな配分なのかを集計する。</p>

<h3>手法 (カラーマッチング集計)</h3>
<p>PDF はベクタ図面で<b>地理座標を持たない</b>ため、Shapefile のように
<code>geopandas.area</code> で面積を直接計算できない。代わりに本記事独自の手法を使う:</p>
<ul>
  <li><b>STEP 1</b>: PyMuPDF (<code>fitz</code>) で PDF ページを 100 dpi (1190×842 pt → 1102×779 px)
      のラスタにレンダリング。</li>
  <li><b>STEP 2</b>: 凡例エリア (右下 x:1010-1042 pt, y:625-695 pt) を 300 dpi でクロップし、
      各階級ラベルの左にあるカラースワッチから median RGB を抽出 → 7 階級の凡例色を得る。</li>
  <li><b>STEP 3</b>: 各ピクセルと 7 階級色との<b>Chebyshev 距離</b> (RGB 各チャネル差の最大値)
      を計算し、距離 ≤ 20 かつ最近傍の階級にそのピクセルを分類。
      さらに彩度 (max-min channel) ≥ 35 のピクセルのみを「塗られた」 と認定し、
      白背景・地形図グレー・山地暗緑を除外する。</li>
  <li><b>STEP 4</b>: 階級ごとのピクセル数を集計。23 PDF × 7 階級 + その他 のテーブルが完成する。</li>
</ul>

<p><b>入力</b>: 23 PDF と凡例 RGB 7 色。<br>
   <b>出力</b>: 23 行 × 7 列のピクセル数テーブル (= <code>L47_pdf_color_hist.csv</code>)。<br>
   <b>限界</b>: ピクセル数は地理面積 (km²) ではない。各 PDF 内の<b>相対比</b>としてのみ意味がある。
   地図の縮尺や図郭サイズは PDF 間で異なるため、PDF 間の絶対比較には注意。
   <b>代替案</b>: 国土地理院の Geo TIFF があれば直接面積計算可能だが、
   DoBoX の本データは PDF 配布のみ。本手法はその制約下での実用解。</p>

<h3>実装の要点</h3>
<ul>
  <li>numpy のベクタ化で 7 階級同時マッチング (<code>np.abs(...).max(-1).argmin(-1)</code>) →
      1 PDF (~1.9 MP) を ~1-2 秒で集計。</li>
  <li>23 PDF 集計結果は JSON にキャッシュ → 2 回目以降は <1 秒で再実行。</li>
  <li>カラー許容差 20 と彩度下限 35 は「PDF レンダリング由来の薄塗りノイズ」を
      吸収しつつ「凡例外の地形図山緑を混同しない」値として実験的に決定。</li>
</ul>
"""

sec4_code1 = code(r"""
# 凡例エリアから 7 階級色を測色
import fitz, numpy as np
doc = fitz.open("水害リスクマップ（瀬野川水系）【浸水深0.0m以上】.pdf")
page = doc[0]
clip = fitz.Rect(1000, 615, 1110, 705)              # 凡例右下のbbox (pt)
mat = fitz.Matrix(300/72.0, 300/72.0)                # 300 dpi
pix = page.get_pixmap(matrix=mat, alpha=False, clip=clip)
arr = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, 3)
# 各凡例ラベルの y_pt → ピクセル y 位置
labels = [("1/5", 633), ("1/10", 642), ("1/30", 651), ("1/50", 661),
          ("1/70", 670), ("1/100", 680), ("max", 690)]
for name, y_pt in labels:
    py = int((y_pt - clip.y0) * 300/72.0)            # ピクセル単位の y
    swatch = arr[max(0,py-3):py+4, 41:175].reshape(-1, 3)  # 左側の色矩形
    mask = ~((swatch[:,0]>=235)&(swatch[:,1]>=235)&(swatch[:,2]>=235))
    rgb = np.median(swatch[mask], axis=0).astype(int)
    print(f"{name:>5} -> RGB {tuple(rgb)}")
""")

sec4_code2 = code(r"""
# 1 PDF を 100 dpi にラスタ化 → 7 階級色マッチングで分類
import fitz, numpy as np

PROB_RGB = np.array([                                  # 上で抽出した 7 階級
    [200,179,223], [246,140,140], [246,188,189],       # 1/5, 1/10, 1/30
    [151,207,246], [182,249,177],                       # 1/50, 1/70
    [255,200, 84], [255,255,179],                       # 1/100, max
], dtype=np.int32)
TOLERANCE = 20                                         # Chebyshev 距離許容
SAT_MIN   = 35                                         # 彩度下限 (max-min channel)

def classify_pdf(pdf_path):
    doc = fitz.open(pdf_path)
    page = doc[0]
    pix = page.get_pixmap(matrix=fitz.Matrix(100/72.0, 100/72.0), alpha=False)
    arr = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, 3).astype(np.int32)
    sat = arr.max(axis=-1) - arr.min(axis=-1)          # 簡易彩度 (R,G,B のレンジ)
    # 各ピクセル × 7 階級 の Chebyshev 距離 (RGB 差の max)
    diffs = np.abs(arr[:, :, None, :] - PROB_RGB[None, None, :, :]).max(axis=-1)
    nearest   = diffs.argmin(axis=-1)                  # (H, W) どの階級に近いか
    nearest_d = diffs.min(axis=-1)                     # (H, W) その距離
    # 「塗られている」 = 距離 ≤ 20 かつ 彩度 ≥ 35
    matched   = (nearest_d <= TOLERANCE) & (sat >= SAT_MIN)
    counts = {i: int((matched & (nearest == i)).sum()) for i in range(7)}
    other = int((~matched).sum())
    doc.close()
    return counts, other
""")

sec4_fig1_text = """
<h3>図 1: PDF プレビュー + 凡例カラー解説</h3>
<p><b>なぜこの図か</b>: 学習者がまず<b>「水害リスクマップとは何か」</b>を視覚で理解するため、
1 PDF (瀬野川 d00) のプレビューと、抽出した 7 階級色のスウォッチを並置する。
PDF の右下に凡例があり、本文中央が地図、という配置を見ることで
「どこから色を抽出したか」が一目で分かる。</p>
"""
sec4_fig1_read = """
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>瀬野川水系の水害リスクマップは A3 横の地図 1 ページ。地図中央が本流域、右下に凡例、左上に説明文。</li>
  <li>凡例 7 色は「色相」ではなく「彩度・明度」で段階を表現する設計。
      高頻度 (1/5) が紫薄、中頻度 (1/30〜1/50) が桃淡〜水色、低頻度 (max) が淡黄。
      色の感覚的な「濃さ」と頻度の高さが直接対応していない (= 政府配色基準は別建て)。</li>
  <li>抽出した RGB はすべて互いに 80 以上離れているため、Chebyshev 距離 18 のマッチングは確実に分離可能。</li>
</ul>
"""

sec4_fig2_text = """
<h3>図 2: 浸水深 3 閾値 × 7 階級 の積層棒</h3>
<p><b>なぜこの図か</b>: H1 (頻度階級が 7 段階全部出現するか) と H2 (深さ閾値で減衰するか) を
1 枚で同時検証する。bar の合計高さ = 浸水深ごとの全浸水範囲、
内部の積層 = 7 階級の構成。</p>
"""
d05_pct = round(100*stack_per_depth["d05"]["total"]/max(stack_per_depth["d00"]["total"],1), 1)
d30_pct = round(100*stack_per_depth["d30"]["total"]/max(stack_per_depth["d00"]["total"],1), 1)
# d05 が d00 より大きい異常があるか?
d05_anomaly = stack_per_depth["d05"]["total"] >= stack_per_depth["d00"]["total"]
d05_max_vs_d00_max = stack_per_depth["d05"]["想定最大規模"] - stack_per_depth["d00"]["想定最大規模"]
sec4_fig2_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>d30 (3.0m+) → d00 (0.0m+) で総ピクセル数は約 {d30_pct}% → 100% に変化 →
      <b>3.0m 以上の深い浸水は全浸水域の約 {d30_pct}%</b> に局在し、
      H2 (d30 ≤ 1/3 = 33.3%) を{"<b>支持</b>" if d30_pct <= 33.3 else ("<b>部分支持</b>" if d30_pct <= 50 else "<b>反証</b>")}。</li>
  <li>{"<b>異常</b>: d05 (= 0.5m+) の塗り総量が d00 (= 0.0m+) を <b>上回る</b> ({d05_pct}%) という" if d05_anomaly else "d05 (= 0.5m+) が d00 (= 0.0m+) を下回り、深さ閾値が厳しくなるほど面積が単調減少する物理的整合性を保つ。"}
      {"物理的に逆向きの結果が出た。これはマッチング・カラーの設計上の<b>制度的アーティファクト</b>と推定される: d00 の PDF では薄黄 (max) の上に薄水色 (1/100) などの中間階級色が重ねて表示される結果、淡黄ピクセルの面積が縮小する。一方 d05 では中間階級が縮退するため、淡黄が露出するピクセルが増える。<b>つまり、PDF はピクセル単位で物理的整合的に設計されておらず、「最頻のみ表示」 設計の副作用が出ている</b>。" if d05_anomaly else ""}</li>
  <li>各 bar の内部積層を見ると、7 階級すべての色が d00, d05 で出現する → <b>H1 (7 階級積層構造) を支持</b>。
      d30 では 1/5 (高頻度) などの一部階級がほぼ 0 になり、
      これは「高頻度浸水エリアは河川直近で深さも 3m に達するエリアは限定的」という
      地形物理的な解釈と整合する。</li>
  <li>「想定最大規模」 (淡黄) のシェアが圧倒的 → これは「最頻のみ表示」 設計から、
      「想定最大規模で初めて浸水するエリア」が他階級より広範であることを意味する。
      つまり<b>「想定最大規模専有エリア」</b> が浸水想定全域の <b>過半</b> を占める。</li>
</ul>
"""

sec4_fig3_text = """
<h3>図 3: 浸水深 3 閾値の階級シェア small multiples</h3>
<p><b>なぜこの図か</b>: 図 2 の積層 bar だけでは「階級ごとの構成比」が読みにくいため、
3 浸水深を別々のサブプロットにして横棒で各階級シェアを直接比較する。</p>
"""
sec4_fig3_read = """
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>3 つのサブプロットは構成比のパターンが酷似 → <b>頻度階級の比率は深さに依存しない</b>。
      これは「同じ場所が同じ頻度プロファイルを保つ」という地形的解釈と整合。</li>
  <li>「想定最大規模」(淡黄) の比率が最大のケースが多い → これは
      「想定最大規模で初めて浸水するエリア」が高頻度エリアより広範であることを示す。
      H5 の「最頻のみ表示」設計と整合 (低頻度の色は高頻度に上書きされず残る)。</li>
  <li>1/5 (紫薄) は最も狭い → 高頻度浸水エリアは河川直近に局在。</li>
</ul>
"""

# Sec 5: RQ2 (高リスクエリアの地理特性)
sec5_intro = f"""
<h3>狙い (RQ2)</h3>
<p>水害リスクマップは {TOTAL_RIVERS} 河川水系をカバーする。
8 リソース群 (= ZIP 配布単位) で見たとき、<b>どの群が浸水想定面積で大きいか</b>、
そして高頻度シェアが大きい群はどこかを定量化する。
これは河川管理の<b>注力配分</b>を反映する研究的に重要なシグナル。</p>

<h3>手法 (頻度プロファイル分析)</h3>
<p>各リソース群について、浸水深 d00 (0.0m+) の集計から以下の 3 指標を計算する:</p>
<ul>
  <li><b>高頻度シェア</b> = (1/5 + 1/10 + 1/30) ピクセル / 総ピクセル × 100 [%]</li>
  <li><b>中頻度シェア</b> = (1/50 + 1/70) ピクセル / 総ピクセル × 100 [%]</li>
  <li><b>低頻度シェア</b> = (1/100 + 想定最大規模) ピクセル / 総ピクセル × 100 [%]</li>
</ul>
<p>これらは合計 100% になる本記事独自の指標。
高頻度シェアが大きい群は「日常的洪水で被害が出やすい流域」を意味し、
低頻度シェアが大きい群は「想定最大規模の備えが特に必要な流域」を意味する。</p>

<p><b>入力</b>: 8 リソース × 7 階級の集計テーブル。<br>
   <b>出力</b>: 8 行 × {{高/中/低}} の頻度プロファイル。<br>
   <b>限界</b>: ピクセル数は PDF ページサイズ・縮尺・図郭定義に依存するため、
   群間の絶対面積比較は近似値である。<b>同一群内</b>の階級比は信頼できる。</p>
"""

sec5_code1 = code(r"""
# 8 リソース別の高/中/低 頻度シェア計算
import pandas as pd

records = []
for rid, (slug, jp) in RESOURCES.items():
    sub = hist_df[(hist_df["rid"]==rid) & (hist_df["depth_key"]=="d00")]
    if len(sub) == 0: continue
    row = sub.iloc[0]
    high = row["px_1/5"] + row["px_1/10"] + row["px_1/30"]
    mid  = row["px_1/50"] + row["px_1/70"]
    low  = row["px_1/100"] + row["px_想定最大規模"]
    sum_ = high + mid + low
    records.append({"rid": rid, "jp": jp,
                    "sum_px": sum_,
                    "high_freq_pct": 100*high/sum_,
                    "mid_freq_pct":  100*mid /sum_,
                    "low_freq_pct":  100*low /sum_})
res_df = pd.DataFrame(records).sort_values("sum_px", ascending=False)
print(res_df.head())
""")

sec5_fig4_text = """
<h3>図 4: 8 リソース群別 階級構成 (積層 stacked bar)</h3>
<p><b>なぜこの図か</b>: 8 群の浸水想定規模 (絶対量) と階級構成 (相対比) を
1 枚で同時に見るには横の積層 bar が最適。長い bar = 大規模流域、
内部の色が階級構成。</p>
"""
top1_name = res_df.iloc[0]["jp_short"][:30]
top1_n = int(res_df.iloc[0]["sum_px"])
sec5_fig4_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>最大規模は<b>{top1_name}</b>群で {top1_n:,} px。</li>
  <li>上位 5 群で全体の <b>{top5_share:.1f}%</b> を占める → H3 (上位 5 で 50% 以上) を{"<b>支持</b>" if top5_share >= 50 else "<b>部分支持</b>"}。</li>
  <li>水系数の多い群 (8 水系の藤井川群など) は当然大きいが、水系あたりの規模に直すと別の順位になる
      (= 表 7 を参照)。<b>水系の長さ</b> が浸水範囲に効いている。</li>
  <li>階級構成は群によってかなり異なる。瀬野川群は中頻度 (水色・緑) が顕著、
      藤井川群は想定最大規模 (淡黄) が支配的。これは流域形状 (V 字 vs 扇状) の
      地形的な差を反映する可能性がある。</li>
</ul>
"""

sec5_fig5_text = """
<h3>図 5: 高/中/低 頻度シェアの 3 軸散布</h3>
<p><b>なぜこの図か</b>: 8 群の頻度プロファイルを<b>1 つの平面</b>に投影し、
類似群と例外群を視覚化する。X = 高頻度, Y = 低頻度, 色 = 中頻度, バブル面積 = 総 px。
4 次元情報を 1 枚で読める。</p>
"""
sec5_fig5_read = """
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>群は X-Y 平面上で対角線 (高頻度と低頻度はトレードオフ) の周辺に分布する。
      高頻度シェアが大きい群は低頻度シェアが小さい (= 同じ深さ閾値での全浸水範囲を分配する制約)。</li>
  <li>右下 (高頻度大・低頻度小) の群は「日常的洪水が多発する流域」、
      左上 (高頻度小・低頻度大) の群は「想定最大規模の盲点が多い流域」と読める。</li>
  <li>外れ値があれば、それは制度的設計や流域形状に特殊性がある群と疑える。</li>
</ul>
"""

# Sec 6: RQ3 (既存浸水想定との比較)
n_max_poly = ext_meta.get("max_souteisaidai", {}).get("n", 0)
n_plan_poly = ext_meta.get("plan_keikakukibo", {}).get("n", 0)
sec6_intro = f"""
<h3>狙い (RQ3)</h3>
<p>本マップ (PDF) と既存浸水想定 Shapefile (L4-L11 で扱う想定最大規模 = {n_max_poly} polygons / 計画規模 = {n_plan_poly} polygons)
の<b>関係</b>を整理する。両者は同じ「河川氾濫」を扱うが配布フォーマットが違い、
頻度の解像度も違う。何が補完で何が欠落かを明確にする。</p>

<h3>手法 (フォーマット欠落マトリクス)</h3>
<p>本記事独自手法 — <b>「フォーマット欠落マトリクス」</b>:</p>
<ul>
  <li><b>STEP 1</b>: 既存 Shapefile (想定最大規模 / 計画規模) を <code>geopandas</code> で読み込み、
      件数・座標系・bbox を取得 (空間処理は重いので polygon 詳細処理は行わない、要件 S 遵守)。</li>
  <li><b>STEP 2</b>: 7 つの頻度階級それぞれについて、
      「本マップ (PDF) で取得可能か」「Shapefile で取得可能か」を ○/× で記録。</li>
  <li><b>STEP 3</b>: マトリクスから「<b>PDF 専有情報</b>」 (PDF にあるが Shapefile にない階級)
      を同定する。これが H4 の検証になる。</li>
</ul>
<p>注意: Shapefile を本記事内で実際に空間 overlay することはしない。
それは L8 (河川 × 津波 × 盛土) や L11 (トリプルハザード) で別途扱われており、
本記事のスコープを守る。本記事は「フォーマット差」というメタ問題に集中する。</p>
"""

sec6_code = code(r"""
# 既存 Shapefile の bounds と件数のみ取得 (重い空間処理は避ける = 要件S)
import geopandas as gpd

shp_max  = gpd.read_file("data/extras/flood_shp/shinsui_souteisaidai/shinsui_souteisaidai.shp")
shp_plan = gpd.read_file("data/extras/flood_shp/shinsui_keikaku/shinsui_keikakukibo.shp")

print(f"想定最大規模: {len(shp_max)} polygons, CRS={shp_max.crs}, bbox={shp_max.total_bounds}")
print(f"計画規模:     {len(shp_plan)} polygons, CRS={shp_plan.crs}, bbox={shp_plan.total_bounds}")

# SINSUIRANK 列があれば浸水深ランク, ない場合は dataset 単独 = 1 段階のみ
# どちらの Shapefile も 「年超過確率の階級」 列を持たない → 7 階級情報は欠落と確認
""")

sec6_fig6_text = f"""
<h3>図 6: 既存浸水想定 Shapefile の空間範囲 (bbox 比較)</h3>
<p><b>なぜこの図か</b>: 本マップ (PDF) は GIS 座標を持たないため空間的な
「Shapefile との差」を直接的に重ねられない。代わりに既存 Shapefile の bbox を
広島県境上に描き、本マップが<b>同じ空間範囲</b>を別フォーマットで記述していることを示す。
PDF の制約 (=「面積は出せても座標が紐付かない」) も視覚的に説明する。</p>
"""
max_x_km = round((ext_meta.get("max_souteisaidai",{}).get("bounds",[0,0,0,0])[2]
                   - ext_meta.get("max_souteisaidai",{}).get("bounds",[0,0,0,0])[0])/1000, 1)
max_y_km = round((ext_meta.get("max_souteisaidai",{}).get("bounds",[0,0,0,0])[3]
                   - ext_meta.get("max_souteisaidai",{}).get("bounds",[0,0,0,0])[1])/1000, 1)
sec6_fig6_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>想定最大規模 Shapefile の bbox は広島県の主要範囲 ({max_x_km} km × {max_y_km} km) を覆い、
      ほぼ全域に及ぶ。</li>
  <li>計画規模 Shapefile も同じ範囲だがやや狭い → 計画規模が想定最大規模の部分集合という制度設計と整合。</li>
  <li>本マップ (PDF) は同じ空間範囲を扱うが、bbox を描けない (= GIS 座標がない)。
      これは図上の「<b>欠落</b>」 — 同じ情報が別フォーマットでも存在するが、
      <b>機械可読性</b> が大きく違うことを示す。</li>
</ul>
"""

sec6_fig7_text = """
<h3>図 7: 23 PDF × 7 階級 行内正規化ヒートマップ</h3>
<p><b>なぜこの図か</b>: 23 PDF (8 群 × 3 深さ) の<b>個別パターン</b>を 1 枚で読みたい。
行ごとに正規化して各 PDF 内のシェア (%) を表示すると、群間の差・深さ間の差が両方見える。</p>
"""
sec6_fig7_read = """
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>同じ群 (rid) 内の 3 行 (d00, d05, d30) はおおむね似たパターン → 深さ閾値が変わっても
      頻度の構成比は不変。これは前述の「同じ場所が頻度・深さの両面で支配される」と整合。</li>
  <li>群間では「想定最大規模」と「1/100」のシェアが大きく異なる。
      これは流域形状や河川改修史の差を反映する。</li>
  <li>「中間頻度」 (1/50, 1/70) は全 PDF で共通して<b>低シェア</b> →
      水文設計上の「節目」 (1/30 と 1/100) の中間に位置するため、
      想定面積が両側に「吸収」される傾向。</li>
</ul>
"""

# Sec 7: 仮説検証
sec7 = (
    "<p>本記事の 5 仮説と観測結果の照合:</p>"
    + df_to_html(T_hypo)
    + "<h3>3 RQ × 3 結論</h3>"
    "<ul>"
    "<li><b>RQ1 結論</b>: 水害リスクマップは <b>「同一ピクセルに最頻のみ表示」</b> という設計の頻度地図である。"
    f"7 階級すべてが d00 PDF 上に共存し、深さ閾値を変えると面積は減衰するが構成比は保たれる "
    f"(d30/d00 = {d30_pct}%, d05/d00 = {d05_pct}%)。</li>"
    "<li><b>RQ2 結論</b>: 8 リソース群の浸水想定規模は<b>偏った分布</b>を示し、"
    f"上位 5 群で全体の {top5_share:.1f}% を占める。水系数の多い群が大きいのは自明だが、"
    "「水系あたり」 で見ると流域形状 (V 字 vs 扇状) や河川長が効く。</li>"
    "<li><b>RQ3 結論</b>: 既存 Shapefile (L4-L11) は<b>2 段階</b>の頻度のみを持ち、"
    "本マップが提示する <b>中間頻度 5 段階 (1/5・1/10・1/30・1/50・1/70)</b> "
    "は Shapefile 由来データから取得不能。<b>フォーマット欠落</b>が大きい。</li>"
    "</ul>"
)

# Sec 8: 発展課題
sec8 = """
<h3>結果X → 新仮説Y → 課題Z (3 RQ × 1 課題以上)</h3>

<h4>発展課題 1 (RQ1 由来)</h4>
<ul>
  <li><b>結果 X</b>: PDF の 7 階級色は完全に分離可能で、Chebyshev 距離 20 + 彩度 35 のマッチングは堅牢に機能した。</li>
  <li><b>新仮説 Y</b>: 同じカラーマッチング法を、国土地理院や他県の水害リスクマップ PDF にも適用すれば、
      <b>全国一括の頻度地図比較</b>が可能になるはず。配色基準が統一されている地区とそうでない地区が見えるかもしれない。</li>
  <li><b>課題 Z</b>: 国交省全国版の水害リスクマップ PDF を 10 件以上集め、それぞれから 7 階級色を抽出し、
      RGB の標準偏差 (=都道府県間の配色ばらつき) を計算する。0 に近ければ全国統一、
      離れていれば各都道府県が独自に配色を決めている証拠。</li>
</ul>

<h4>発展課題 2 (RQ2 由来)</h4>
<ul>
  <li><b>結果 X</b>: 8 リソース群の頻度プロファイルは群によって大きく異なる。</li>
  <li><b>新仮説 Y</b>: 流域形状 (V 字 / 扇状 / 樹枝状) と頻度プロファイルの関連が出るはず。
      V 字流域は本流近接の高頻度浸水が支配的、扇状地は想定最大規模の広範浸水が支配的、
      樹枝状は中頻度シェアが大きい、と予想。</li>
  <li><b>課題 Z</b>: L40 (標高) や L39 (傾斜) のラスタを使って、各水系の流域形状指標
      (Compactness, Drainage density) を計算し、本記事で得た 8 群の頻度プロファイルと
      Pearson 相関を取る。0.5 以上の相関があれば仮説支持。</li>
</ul>

<h4>発展課題 3 (RQ3 由来)</h4>
<ul>
  <li><b>結果 X</b>: PDF と Shapefile はフォーマットが違い、頻度解像度に大きな差がある (7 vs 2)。</li>
  <li><b>新仮説 Y</b>: 「PDF を機械可読にする」 = OCR + ベクタ化で頻度階級の Shapefile を
      <b>事後的に再構築</b>できるはず。色マッチングで得た raster はそのまま polygonize できる。</li>
  <li><b>課題 Z</b>: 本記事の <code>classify_pdf</code> 出力を、PDF の地図枠 (内枠座標) と
      作成時の縮尺メタを使って EPSG:6671 に幾何参照し、<code>rasterio.features.shapes</code>
      で polygon 化する。これにより<b>「PDF 由来の Shapefile」</b>が生成され、
      L4-L11 と同じ空間解析が可能になる。実装すれば本データの<b>機械可読化</b>が達成される。</li>
</ul>
"""

# Combine sections
sections = [
    ("学習目標と問い", sec1),
    ("使用データ", sec2),
    ("ダウンロード", sec3),
    ("【RQ1】 リスク階級分布の研究 — 7 階級 × 3 浸水深閾値の積層構造",
     sec4_intro
     + "<h3>STEP 1 のコード</h3>" + sec4_code1
     + "<h3>STEP 3-4 のコード</h3>" + sec4_code2
     + sec4_fig1_text
     + figure("assets/L47_fig1_legend_preview.png", "図 1: PDF プレビュー + 凡例カラー")
     + sec4_fig1_read
     + sec4_fig2_text
     + figure("assets/L47_fig2_depth_class_stack.png", "図 2: 浸水深 × 階級 積層 bar")
     + sec4_fig2_read
     + sec4_fig3_text
     + figure("assets/L47_fig3_depth_share_smallmultiples.png", "図 3: 階級シェア small multiples")
     + sec4_fig3_read
     + "<h3>表: d00 における 7 階級シェア</h3>"
     + df_to_html(d00_share)
     + "<p><b>この表から読み取れること</b>: "
       "「想定最大規模」 が最も大きいシェアを占める。これは「最頻のみ表示」設計から、"
       "「想定最大規模で初めて浸水するエリア」が他階級より広範であることを意味する。"
       "1/5 (高頻度) は最も狭く、河川直近に局在する。</p>"
     + "<h3>表: 浸水深 3 閾値 × 7 階級 の合算</h3>"
     + df_to_html(T_depth_total)
     + "<p><b>この表から読み取れること</b>: "
       "深さが増えるほど全階級の値が縮小するが、構成比は保たれる。"
       "これは「同じ場所が深い場所で重畳的に浸水する」 という地形的構造を示す。</p>"
     + "<h3>表: 頻度階級ごとの浸水深減衰</h3>"
     + df_to_html(T_depth_decay)
     + "<p><b>この表から読み取れること</b>: "
       "d05/d00 は階級によらず {:.0f}% 前後、d30/d00 は {:.0f}% 前後で安定。"
       "つまり「深い浸水を被るエリアは頻度に依らず約{:.0f}%」 という<b>地形主導の比率</b>がある。</p>"
       .format(d05_ratio, d30_ratio, d30_ratio)
    ),
    ("【RQ2】 高リスクエリア地理特性の研究 — 8 リソース群 × 32 水系のプロファイル",
     sec5_intro
     + "<h3>実装コード</h3>" + sec5_code1
     + sec5_fig4_text
     + figure("assets/L47_fig4_resource_class_stack.png", "図 4: 8 リソース群 階級構成")
     + sec5_fig4_read
     + sec5_fig5_text
     + figure("assets/L47_fig5_freq_profile_scatter.png", "図 5: 頻度プロファイル散布")
     + sec5_fig5_read
     + "<h3>表: リソース別 高/中/低 頻度シェア</h3>"
     + df_to_html(T_freq_profile)
     + "<p><b>この表から読み取れること</b>: "
       "高頻度シェアと低頻度シェアは負相関の傾向 (合計 100% の制約)。"
       "中頻度シェアが特に大きい群は「中規模洪水で被害が出やすい」 という"
       "別の特徴を示し、河川改修や流域治水の<b>注力ポイント</b>を示唆する。</p>"
     + "<h3>表: 8 リソースの基本情報</h3>"
     + df_to_html(T_resources)
     + "<p><b>この表から読み取れること</b>: "
       "8 リソース群はそれぞれ 1〜8 河川水系を含み、最大は藤井川群 (8 水系)。"
       "1 リソース = 1 ZIP = 1 ダウンロード単位という配布設計は、"
       "ファイルサイズが 20 MB を超えるため一括 DL ができないという物理的制約から来る。</p>"
    ),
    ("【RQ3】 既存浸水想定との比較研究 — フォーマット欠落の論証",
     sec6_intro
     + "<h3>実装コード</h3>" + sec6_code
     + sec6_fig6_text
     + figure("assets/L47_fig6_external_shp_bbox.png", "図 6: 既存 Shapefile の bbox 比較")
     + sec6_fig6_read
     + sec6_fig7_text
     + figure("assets/L47_fig7_class_heatmap.png", "図 7: 23 PDF × 7 階級 ヒートマップ")
     + sec6_fig7_read
     + "<h3>図 8: 23 PDF small multiples</h3>"
     + "<p><b>なぜこの図か</b>: 23 PDF を 1 枚で並置することで、"
       "群ごと・深さごとのパターン差を視覚的にカタログ化する。</p>"
     + figure("assets/L47_fig8_pdf_small_multiples.png", "図 8: 23 PDF プレビュー一覧")
     + "<p><b>この図から読み取れること</b>: "
       "23 PDF は地図サイズ・図郭が大きく異なる。瀬野川群はやや大きい A3 横、"
       "藤井川群は同じ A3 横でも図郭幅が広い。これは流域の地理的広がりを反映する。"
       "「同じ deta が同じ frame で配布されない」 のは PDF の制約であり、"
       "GIS で扱うには事後的なジオレファレンスが必要 (発展課題 3 参照)。</p>"
     + "<h3>表: フォーマット比較 (PDF vs Shapefile)</h3>"
     + df_to_html(T_compare)
     + "<p><b>この表から読み取れること</b>: "
       "PDF は「機械可読性が低いが頻度解像度が高い」、Shapefile は「機械可読性が高いが頻度解像度が低い」 という"
       "<b>排他関係</b>。両者を補完して使うことが理想だが、現状はそれぞれ独立した制度で運用されている。</p>"
     + "<h3>表: 既存 Shapefile の空間範囲</h3>"
     + df_to_html(T_ext_bounds)
     + "<p><b>この表から読み取れること</b>: "
       f"想定最大規模 Shapefile は {max_x_km} km × {max_y_km} km の範囲をカバーし、"
       "広島県全域 (約 8,479 km²) を含む。本マップ PDF も同じ範囲を扱うが、"
       "GIS 座標がないため bbox を直接出せない。</p>"
     + "<h3>表: 頻度階級 × フォーマット の被覆差</h3>"
     + df_to_html(T_format_gap)
     + "<p><b>この表から読み取れること</b>: "
       "1/5・1/10・1/30・1/50・1/70 の<b>5 階級</b>は PDF にしかない。"
       "つまり Shapefile を使った既存研究 (L4-L11 の浸水想定分析) は"
       "<b>5/7 = 71%</b> の頻度情報を見落としている可能性がある。"
       "本マップの存在意義は<b>フォーマット欠落を埋めること</b>にある。</p>"
    ),
    ("仮説検証総合", sec7),
    ("発展課題", sec8),
]

html = render_lesson(
    num=47,
    title="水害リスクマップ（浸水頻度図）単独 3 研究例分析 — 7 階級 × 32 水系の頻度地図を読む",
    tags=["L47", "PDF解析", "水害リスク", "浸水頻度", "RQ×3", "カラーマッチング"],
    time="35 分",
    level="中級+",
    data_label="DoBoX dataset 1640 (8 リソース, 23 PDF)",
    sections=sections,
    script_filename="L47_flood_risk_map.py",
)

OUT_HTML = LESSONS / "L47_flood_risk_map.html"
OUT_HTML.write_text(html, encoding="utf-8")
print(f"  HTML written: {OUT_HTML}")
print(f"  ({time.time()-t1:.1f}s)", flush=True)


print(f"\n=== L47 DONE in {time.time()-t0:.1f}s ===")
