# -*- coding: utf-8 -*-
"""L65 防潮扉（水門・陸閘）基本情報・維持管理情報 単独 3 研究例分析
       — 広島県内 2,675 件 / 13 市町 / 99 地区海岸の防潮扉の動的防御網を 3 角度で解読

カバー宣言:
  本記事は DoBoX のシリーズ「防潮扉（水門・陸閘）基本情報・維持管理情報」 1 件
  (dataset_id = 1249) を <b>単独</b>で取り上げ、
  広島県内の 2,675 件・13 市町に整備された<b>防潮扉</b>を
  3 つの独立した研究角度 (RQ1 / RQ2 / RQ3) で並列に分析する。
  L64 (海岸保全施設, 静的防御) と対をなす<b>「動的防御 = 開閉操作で起動する最後のバリア」</b>を
  制度・地理・分担の 3 軸から立体的に描く。

  「防潮扉」 とは:
    沿岸の堤防・防潮堤を貫通する道路や河口の<b>開口部</b>に設置する開閉式の扉。
    平常時は開、台風・高潮・津波の予報時に閉じることで内陸を守る<b>動的施設</b>。
    本データには 5 種類: 陸閘 / 角落し / 開口部 / 樋門 / 招扉 が含まれる。
    L64 海岸保全施設 (=連続して立つ静的構造物) とは異なり、<b>「人手 or 自動制御で開閉」</b>するのが
    本質的特徴。閉じ忘れ・操作遅れがそのまま被害に直結するため、平時の<b>点検・訓練・操作員配置</b>が
    防御能力を支配する。

  本記事は L64 (海岸保全施設) と<b>厳密に区別</b>:
    L64 = <b>静的防御</b> (護岸・防潮堤・離岸堤 等, 1,645 件)
    L65 = <b>動的防御</b> (陸閘・水門・樋門 等, 2,675 件)
    両者は同じ広島県沿岸を共有するが、L64 が「常時防御」、L65 が「閉鎖時防御」 で機能が異なる。
    実データでも L64 護岸 90% (静的) vs L65 陸閘 81% (動的, 閉鎖式) と<b>動静で分極</b>する。
    本研究 RQ3 で両者の役割分担を空間統計化する。

  本記事は L44 (高潮浸水想定) / L49 (津波浸水想定) と<b>厳密に独立</b>。
  RQ2 で<b>海起源 2 ハザード (高潮 + 津波)</b> に対する扉のカバレッジを比較。

研究の問い (3 RQ):
  RQ1 (主研究): 広島県の防潮扉の<b>構造 — 種類・規模・地理分布</b>はどう描けるか?
       2,675 件の防潮扉を 5 種類 (陸閘 / 角落し / 開口部 / 樋門 / 招扉) ・所管 5 区分 ・
       市町 13 ・地区海岸 99 ・潮位区分 13 別に集計し、地理分布を多角度に定量化する。
       特に「県の動的防御網」 の物理的形状を初めて立体的に描く。

  RQ2 (副研究 1): 防潮扉は<b>L44 高潮想定 / L49 津波想定</b>の脅威に対し
       どの程度カバーしているか?
       扉の geometry (POINT) と高潮 / 津波の浸水想定 polygon を空間結合し、
       「想定エリア + 扉あり (= 閉鎖時防御済)」 「想定エリア + 扉なし (= 隙間)」 の
       バランスを市町別に可視化する。
       海起源 2 ハザード (高潮 + 津波) に対する<b>扉密度</b>を読む。

  RQ3 (副研究 2): L64 海岸保全施設 (1,645 件 / 静的) と L65 防潮扉 (2,675 件 / 動的) の
       <b>動静役割分担</b>はどう描けるか?
       施設密度・地理分布を 2 シリーズ間で比較し、
       「常時連続防御 (L64 静的)」 と「閉鎖時開口部防御 (L65 動的)」 の役割分担を抽出。
       両者の重複 / 補完エリアを地理可視化し、県の海岸防御の総合戦略 (動 + 静 = 完全) を読む。

仮説 (5):
  H1 (陸閘支配, RQ1): 防潮扉の<b>80% 以上が陸閘</b>。
       これは「堤防を横断する道路の開口部を閉じる」 という防潮扉の本旨を反映。
       角落し (= 角材を落として閉じる小規模型) が 2 番手、樋門 (=排水兼用) が小数。

  H2 (上位 3 市町集中, RQ1): 上位 3 市町 (尾道・呉・広島) で
       全体の <b>40% 超</b>。これは多島海 + 大規模都市沿岸という
       2 地形類型が動的施設の整備を支配することを示す。

  H3 (高潮 ⊃ 津波カバレッジ, RQ2): 防潮扉は<b>高潮想定エリア</b>に
       より強く重なる (高潮カバー率 > 津波カバー率)。
       これは防潮扉が高潮防御を主目的とする歴史的経緯を反映。
       L64 と同じ +20pt 級の差が観測されるか。

  H4 (動静密度比, RQ3): L65 防潮扉密度 (件/km) は L64 静的施設密度を
       <b>上回る</b>。これは「動的施設は開口部 1 箇所毎に 1 つ必要」 で密に配置され、
       「静的施設は連続 1 本で長距離をカバー」 する性質の差を反映。
       具体的には L65 / L64 件数比 = 2675 / 1645 ≈ 1.63 倍。

  H5 (港湾系所管支配, RQ3): L65 防潮扉も L64 海岸保全と同様に
       <b>港湾系所管 (港湾 + 漁港) が 70% 超</b>。同じ法体系 (海岸法 + 港湾法) の管理下で、
       静的 + 動的の 2 系統を同じ管理者が運用している。

要件 S 準拠 (1 分以内完走):
  - 全データ 2,675 件 / 5 種類 / 5 所管 / 13 市町 → 軽量
  - POINT geometry のみ (LineString/Polygon 不要) → sjoin 高速
  - L44 / L49 既キャッシュを再利用 (admin_diss / diss_max / tsunami raw shp)
  - L64 既扱の集計結果 (L64_kind_summary.csv) も再利用
  - 重い前処理は無し。本スクリプト 1 本で完結 (~30 秒目標)

要件 T 準拠 (位置情報 = POINT = 地図必須):
  - RQ1: 県全域 種類別マップ、市町別 choropleth、地区海岸別 ランキング
  - RQ2: 高潮 + 津波 + 扉 重ね合わせマップ、想定×扉 small multiples
  - RQ3: L64 + L65 重ね合わせマップ、動静密度比較

要件 Q 準拠: 図 8 / 表 12 (3 RQ × 多角度: 構造 / カバレッジ / 動静分担)

データ仕様:
  - dataset 1249: 防潮扉（水門・陸閘）基本情報・維持管理情報 (CSV)
  - resource 32493: 340006_sluice_20230509.csv (497 KB)
  - 形式: CSV, 2,675 行 × 12 列
  - 列: 防潮扉ID, 所管, 事務所名, 地区名, 港湾名, 地区海岸名, 種類, 市町名,
       潮位区分, GIS情報, 開始位置緯度, 開始位置経度
  - 種類: 陸閘 2175 / 角落し 360 / 開口部 57 / 樋門 45 / 招扉 29
  - 所管: 港湾 1821 / 河川 340 / 漁港 297 / 道路 208 / 農林 2
  - GIS情報: 2,282 件 (85.3%) で WKT POINT
            残り 393 件は緯度経度のみで POINT 文字列なし (= 緯度経度から復元可)
  - 公表年月日: 2023-05-09
  - 作成主体: 港湾漁港整備課
  - ライセンス: クリエイティブ・コモンズ表示 (CC-BY)

メモリ対策: Figure ごとに plt.close('all') で確実に解放。
"""
from __future__ import annotations
import sys, time
from pathlib import Path

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

import numpy as np
import pandas as pd
import geopandas as gpd
import shapely
from shapely.wkt import loads as wkt_loads
from shapely.geometry import Point
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
from matplotlib.lines import Line2D

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

t_all = time.time()
print("=== L65 防潮扉（水門・陸閘）単独 3 研究例分析 ===", flush=True)

# =============================================================================
# 0. 定数・パス
# =============================================================================
TARGET_CRS = "EPSG:6671"  # JGD2011 平面直角第 III 系
DATA_DIR = ROOT / "data" / "extras" / "L65_floodgates"
DATA_DIR.mkdir(parents=True, exist_ok=True)
CSV_PATH = DATA_DIR / "340006_sluice_20230509.csv"
DATASET_ID = 1249
RESOURCE_ID = 32493

ADMIN_GPKG = ROOT / "data" / "extras" / "L44_storm_surge" / "_cache" / "admin_diss.gpkg"
L44_MAX_GPKG = ROOT / "data" / "extras" / "L44_storm_surge" / "_cache" / "diss_max.gpkg"
L49_RAW_SHP = ROOT / "data" / "extras" / "tsunami_extracted" / \
              "340006_tsunami_inundation_assumption_map_20251203" / "浸水メッシュ.shp"
L64_KIND_CSV = ASSETS / "L64_kind_summary.csv"
L64_FACIL_CSV = ASSETS / "L64_all_facilities.csv"
# 元データ (L65 RQ3 で動静比較に使用)
L64_RAW_CSV = ROOT / "data" / "extras" / "L64_coast_protection" / \
              "340006_coastal_equipment_20230509.csv"

# 5 種類の防潮扉
KIND_ORDER = ["陸閘", "角落し", "開口部", "樋門", "招扉"]
KIND_FUNCTION = {
    "陸閘":   "堤防横断道路の閉鎖 (= 平常時は車両通行、台風時に閉じる)",
    "角落し": "小規模開口部に角材を上から落として閉じる (= 操作簡易・低コスト)",
    "開口部": "開閉機構未明の堤防貫通開口部 (= 一部は土嚢で塞ぐ簡易型)",
    "樋門":   "排水機能兼用の小型扉 (= 内水排除と高潮遮断の両立)",
    "招扉":   "風雨時に閉じる小型扉 (= 通常は開けておく漁港等の小規模型)",
}
KIND_COLOR = {
    "陸閘":   "#cf222e",  # 赤 (主役、堤防横断)
    "角落し": "#f5a524",  # オレンジ (簡易閉鎖)
    "開口部": "#1a7f37",  # 緑 (開放型)
    "樋門":   "#0969da",  # 青 (排水兼用)
    "招扉":   "#9467bd",  # 紫 (小型)
}

# 5 所管区分
JURISDICTION_ORDER = ["港湾", "河川", "漁港", "道路", "農林"]
JURISDICTION_COLOR = {
    "港湾": "#0969da", "河川": "#cf222e", "漁港": "#1a7f37",
    "道路": "#9a6700", "農林": "#8250df",
}

# 市町コード (L64 と同じ規則)
CITY_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: "世羅町", -1: "県外/海上",
}
COASTAL_CITIES = {101, 102, 103, 104, 107, 108, 202, 203, 204, 205,
                  207, 211, 213, 215, 304, 309}


def rollup_to_city(cd):
    """広島市の 8 区を「広島市」 にロールアップした名前を返す"""
    nm = CITY_NAME.get(int(cd), "?")
    if nm.startswith("広島市"):
        return "広島市"
    return nm


# =============================================================================
# 1. データ取得 + 読込
# =============================================================================
print("\n[1] データ取得 + 読込", flush=True)
t1 = time.time()

# DoBoX から取得 (ローカルにあればスキップ)
try:
    ensure_dataset(CSV_PATH, dataset_id=DATASET_ID, resource_id=RESOURCE_ID,
                   label="L65 防潮扉")
except Exception as e:
    print(f"  WARN: ensure_dataset 失敗: {e}", flush=True)

df_raw = pd.read_csv(CSV_PATH, encoding="utf-8-sig")
print(f"  全行数: {len(df_raw)}, 列数: {df_raw.shape[1]}", flush=True)
print(f"  種類:     {df_raw['種類'].value_counts().to_dict()}", flush=True)
print(f"  所管:     {df_raw['所管'].value_counts().to_dict()}", flush=True)
print(f"  市町数:   {df_raw['市町名'].nunique()}", flush=True)
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 2. POINT geometry 構築 (WKT があれば優先、無ければ緯度経度から作成)
# =============================================================================
print("\n[2] POINT geometry 構築", flush=True)
t2 = time.time()


def build_point(row):
    s = row["GIS情報"]
    if isinstance(s, str) and s.strip().startswith("POINT"):
        try:
            return wkt_loads(s)
        except Exception:
            pass
    # GIS情報が無くても 緯度経度 が有効ならそこから POINT 作成
    lat = row["開始位置緯度"]
    lon = row["開始位置経度"]
    try:
        if pd.notna(lat) and pd.notna(lon) and 33 <= lat <= 35 and 132 <= lon <= 134:
            return Point(float(lon), float(lat))
    except Exception:
        pass
    return None


df_raw["geometry"] = df_raw.apply(build_point, axis=1)
gdf = df_raw.dropna(subset=["geometry"]).copy()
gdf = gpd.GeoDataFrame(gdf, geometry="geometry", crs="EPSG:4326").to_crs(TARGET_CRS)

n_with_geom = len(gdf)
n_total = len(df_raw)
n_wkt_only = int(df_raw["GIS情報"].astype(str).str.startswith("POINT").sum())
n_recovered = n_with_geom - n_wkt_only
print(f"  geom 有効: {n_with_geom} / {n_total} ({100*n_with_geom/n_total:.1f}%)",
      flush=True)
print(f"  内訳: WKT 由来 {n_wkt_only} + 緯度経度復元 {n_recovered}", flush=True)
print(f"  ({time.time()-t2:.1f}s)", flush=True)


# =============================================================================
# 3. 市町 sjoin (admin polygon に空間結合)
# =============================================================================
print("\n[3] 市町 sjoin", flush=True)
t3 = time.time()

admin = gpd.read_file(ADMIN_GPKG).to_crs(TARGET_CRS)
admin["NAME"] = admin["CITY_CD"].map(CITY_NAME).fillna("?")

# POINT は単純 within 判定
gdf_pts_for_join = gdf[["種類", "geometry"]].copy()
joined = gpd.sjoin(gdf_pts_for_join, admin[["CITY_CD", "geometry"]],
                    how="left", predicate="within")

# sjoin 結果の重複インデックスを除去 (境界上の点は複数 polygon に属することがある)
joined = joined[~joined.index.duplicated(keep="first")]
gdf = gdf.reset_index(drop=True)
gdf["CITY_CD"] = joined.reset_index(drop=True)["CITY_CD"].fillna(-1).astype(int)
gdf["市町名_空間"] = gdf["CITY_CD"].apply(rollup_to_city)
# 元 CSV にも市町名列がある → どちらを採用するかを記録 (基本は CSV 値を信頼)
gdf["市町名"] = gdf["市町名"].fillna("?")
print(f"  sjoin 一致 (-1 を除く): "
      f"{(gdf['CITY_CD'] != -1).sum()} / {n_with_geom}", flush=True)
print(f"  CSV 市町名 vs 空間市町名 一致率: "
      f"{(gdf['市町名'] == gdf['市町名_空間']).sum() / n_with_geom * 100:.1f}%",
      flush=True)
print(f"  ({time.time()-t3:.1f}s)", flush=True)


# =============================================================================
# 4. RQ1 集計: 構造・規模・地理分布
# =============================================================================
print("\n[4] RQ1 集計", flush=True)
t4 = time.time()

n_total_gates = len(df_raw)
n_kinds = df_raw["種類"].nunique()
n_jurisdictions = df_raw["所管"].nunique()
n_cities = df_raw["市町名"].nunique()
n_districts = df_raw["地区海岸名"].nunique()
n_offices = df_raw["事務所名"].nunique()
n_ports = df_raw["港湾名"].nunique()
n_tide_zones = df_raw["潮位区分"].nunique()

# 種類別件数
kind_count = df_raw["種類"].value_counts().reindex(KIND_ORDER, fill_value=0)
kind_geom_pct = (gdf["種類"].value_counts() /
                  df_raw["種類"].value_counts() * 100)

T_kind = pd.DataFrame({
    "種類": KIND_ORDER,
    "件数": kind_count.values.astype(int),
    "件数シェア_%": (kind_count.values / n_total_gates * 100).round(1),
    "geom率_%": [round(kind_geom_pct.get(k, 0), 1) for k in KIND_ORDER],
    "物理機能": [KIND_FUNCTION[k] for k in KIND_ORDER],
})

# H1 検証: 陸閘シェア
rikko_share = kind_count["陸閘"] / n_total_gates * 100
print(f"  陸閘シェア: {rikko_share:.1f}% (H1 = 80% 以上?)", flush=True)

# 所管別件数
juris_count = df_raw["所管"].value_counts().reindex(JURISDICTION_ORDER, fill_value=0)
T_juris = pd.DataFrame({
    "所管": JURISDICTION_ORDER,
    "件数": juris_count.values.astype(int),
    "件数シェア_%": (juris_count.values / n_total_gates * 100).round(1),
})

# 市町別ランキング (CSV値ベース)
city_count = (df_raw.groupby("市町名").size()
              .reset_index(name="件数")
              .sort_values("件数", ascending=False)
              .reset_index(drop=True))
city_count["順位"] = np.arange(1, len(city_count) + 1)
city_count = city_count[["順位", "市町名", "件数"]]

top3_city_share = city_count.head(3)["件数"].sum() / n_total_gates * 100
print(f"  Top 3 市町シェア: {top3_city_share:.1f}% (H2 = 40% 超?)", flush=True)

# 地区海岸別ランキング (Top 15)
district_count = df_raw["地区海岸名"].value_counts()
district_top = pd.DataFrame({
    "順位": np.arange(1, 16),
    "地区海岸名": district_count.head(15).index,
    "件数": district_count.head(15).values,
}).reset_index(drop=True)

# 港湾別 ランキング Top 12
port_count = df_raw["港湾名"].value_counts().head(12)
port_top = pd.DataFrame({
    "順位": np.arange(1, len(port_count) + 1),
    "港湾名": port_count.index,
    "件数": port_count.values,
})

# 事務所別件数
office_count = df_raw["事務所名"].value_counts()

# 種類 × 所管 クロス表
T_kind_juris = (df_raw.groupby(["種類", "所管"]).size()
                .unstack(fill_value=0)
                .reindex(index=KIND_ORDER, columns=JURISDICTION_ORDER, fill_value=0))

print(f"  ({time.time()-t4:.1f}s)", flush=True)


# =============================================================================
# 5. RQ2 集計: 高潮 (L44) / 津波 (L49) カバレッジ
# =============================================================================
print("\n[5] RQ2 集計: 高潮/津波カバレッジ", flush=True)
t5 = time.time()

l44_max = None
if L44_MAX_GPKG.exists():
    l44_max = gpd.read_file(L44_MAX_GPKG).to_crs(TARGET_CRS)
    print(f"  L44 高潮想定 (max): {len(l44_max)} polygon, "
          f"合計 {l44_max['area_km2'].sum():.1f} km²", flush=True)

l49_full_geoms = None
if L49_RAW_SHP.exists():
    print(f"  L49 元 Shapefile 読込 (~6 秒, 1.25M polygon)", flush=True)
    import pyogrio
    l49_full_geoms = pyogrio.read_dataframe(L49_RAW_SHP, columns=[])
    l49_full_geoms = gpd.GeoDataFrame(l49_full_geoms,
                                        crs=l49_full_geoms.crs).to_crs(TARGET_CRS)


def _flag_within_union(points_gdf, target_gdf):
    if target_gdf is None:
        return np.zeros(len(points_gdf), dtype=bool)
    union = shapely.unary_union(target_gdf.geometry.values)
    cx = points_gdf.geometry.x.values
    cy = points_gdf.geometry.y.values
    return np.array([shapely.intersects(shapely.Point(x, y), union)
                     for x, y in zip(cx, cy)])


def _flag_within_strtree(points_gdf, full_polygons_gdf):
    if full_polygons_gdf is None:
        return np.zeros(len(points_gdf), dtype=bool)
    polys = full_polygons_gdf.geometry.values
    tree = shapely.STRtree(polys)
    cx = points_gdf.geometry.x.values
    cy = points_gdf.geometry.y.values
    flags = np.zeros(len(points_gdf), dtype=bool)
    for i, (x, y) in enumerate(zip(cx, cy)):
        pt = shapely.Point(x, y)
        cand = tree.query(pt)
        for j in cand:
            if shapely.intersects(pt, polys[j]):
                flags[i] = True
                break
    return flags


gdf["in_l44"] = _flag_within_union(gdf, l44_max)
if l49_full_geoms is not None:
    gdf["in_l49"] = _flag_within_strtree(gdf, l49_full_geoms)
else:
    gdf["in_l49"] = np.zeros(len(gdf), dtype=bool)

n_geom = len(gdf)
n_in_l44 = int(gdf["in_l44"].sum())
n_in_l49 = int(gdf["in_l49"].sum())
n_in_both = int((gdf["in_l44"] & gdf["in_l49"]).sum())
n_in_either = int((gdf["in_l44"] | gdf["in_l49"]).sum())
n_in_neither = n_geom - n_in_either

l44_cover_pct = 100 * n_in_l44 / n_geom
l49_cover_pct = 100 * n_in_l49 / n_geom
print(f"  高潮想定内: {n_in_l44} / {n_geom} ({l44_cover_pct:.1f}%)", flush=True)
print(f"  津波想定内: {n_in_l49} / {n_geom} ({l49_cover_pct:.1f}%)", flush=True)
print(f"  両方:       {n_in_both} ({100*n_in_both/n_geom:.1f}%)", flush=True)
print(f"  どちらも外: {n_in_neither} ({100*n_in_neither/n_geom:.1f}%)", flush=True)

# 市町別 高潮 vs 津波 カバレッジ
city_cov = (gdf.groupby("市町名")
            .agg(扉数=("種類", "size"),
                 高潮内=("in_l44", "sum"),
                 津波内=("in_l49", "sum"))
            .reset_index())
city_cov["高潮率_%"] = (city_cov["高潮内"] / city_cov["扉数"] * 100).round(1)
city_cov["津波率_%"] = (city_cov["津波内"] / city_cov["扉数"] * 100).round(1)
city_cov = city_cov.sort_values("扉数", ascending=False).reset_index(drop=True)

# 種類別 高潮/津波 カバレッジ
kind_cov = (gdf.groupby("種類")
            .agg(扉数=("CITY_CD", "size"),
                 高潮内=("in_l44", "sum"),
                 津波内=("in_l49", "sum"))
            .reindex(KIND_ORDER).reset_index())
kind_cov["高潮率_%"] = (kind_cov["高潮内"] / kind_cov["扉数"] * 100).round(1)
kind_cov["津波率_%"] = (kind_cov["津波内"] / kind_cov["扉数"] * 100).round(1)

print(f"  ({time.time()-t5:.1f}s)", flush=True)


# =============================================================================
# 6. RQ3 集計: L64 静的 vs L65 動的 役割分担
# =============================================================================
print("\n[6] RQ3 集計: L64 静的 vs L65 動的", flush=True)
t6 = time.time()

l64_kind = None
l64_facil_df = None  # 元データ (RQ3 比較の母数 = 1645 件)
l64_facil_with_city = None  # 市町分配済み (RQ3 市町別比較 = sjoin で算出)
if L64_KIND_CSV.exists():
    l64_kind = pd.read_csv(L64_KIND_CSV, encoding="utf-8-sig")
    print(f"  L64 既扱 種類サマリ読込: {len(l64_kind)} 行", flush=True)
# RQ3 比較用 1: 元データ (1645 件) を読込 (件数 + 所管シェア用)
if L64_RAW_CSV.exists():
    l64_facil_df = pd.read_csv(L64_RAW_CSV, encoding="utf-8-sig")
    print(f"  L64 元データ読込: {len(l64_facil_df)} 件", flush=True)
# RQ3 比較用 2: 元データを WKT パース → centroid sjoin で市町分配
# (assets CSV は重複あるので使わない)
if l64_facil_df is not None:
    l64_g = l64_facil_df.copy()
    l64_g["geometry"] = l64_g["GIS情報"].apply(
        lambda s: wkt_loads(s) if isinstance(s, str) and s.strip() else None
    )
    l64_g = l64_g.dropna(subset=["geometry"])
    l64_gdf_for_join = gpd.GeoDataFrame(
        l64_g, geometry="geometry", crs="EPSG:4326"
    ).to_crs(TARGET_CRS)
    l64_gdf_for_join["_cent"] = l64_gdf_for_join.geometry.centroid
    l64_pts = l64_gdf_for_join.set_geometry("_cent")[["施設種類", "_cent"]].rename(
        columns={"_cent": "geometry"}
    )
    l64_pts = gpd.GeoDataFrame(l64_pts, geometry="geometry", crs=TARGET_CRS)
    l64_joined = gpd.sjoin(
        l64_pts, admin[["CITY_CD", "geometry"]], how="left", predicate="within"
    )
    l64_joined = l64_joined[~l64_joined.index.duplicated(keep="first")]
    l64_gdf_for_join = l64_gdf_for_join.reset_index(drop=True)
    l64_gdf_for_join["CITY_CD"] = l64_joined.reset_index(drop=True)["CITY_CD"].fillna(-1).astype(int)
    l64_gdf_for_join["市町名"] = l64_gdf_for_join["CITY_CD"].apply(rollup_to_city)
    l64_facil_with_city = l64_gdf_for_join
    print(f"  L64 市町分配 (geom 有 {len(l64_gdf_for_join)}): "
          f"{(l64_gdf_for_join['CITY_CD']!=-1).sum()} 件 sjoin 一致", flush=True)

# 動静比較表: L64 (静) vs L65 (動)
T_dynamic_static = pd.DataFrame([
    ("件数",
     f"{len(l64_facil_df) if l64_facil_df is not None else 1645} 件",
     f"{n_total_gates} 件"),
    ("件数比 (L65 / L64)",
     "1.00 (基準)",
     f"{n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f}"),
    ("施設の動作",
     "<b>常時連続防御</b> (= 静的)",
     "<b>閉鎖時開口部防御</b> (= 動的)"),
    ("主役施設",
     "護岸 (約 90%)",
     f"陸閘 ({rikko_share:.0f}%)"),
    ("整備位置",
     "海岸線そのもの (連続)",
     "堤防の<b>開口部</b> (点)"),
    ("形状",
     "LineString / MultiPolygon (線/面)",
     "POINT (点)"),
    ("操作",
     "操作不要 (受動防御)",
     "<b>人手 or 自動制御で開閉</b>"),
    ("故障モード",
     "経年劣化 / 越波 / 倒壊",
     "<b>閉め忘れ / 操作遅れ / 開閉不良</b>"),
    ("根拠法",
     "海岸法 (1956)",
     "海岸法 + 港湾法 等 (施設の所管法)"),
    ("点検頻度の重要性",
     "中 (構造劣化の確認)",
     "<b>高 (= 操作試験 + 訓練必須)</b>"),
    ("dataset_id",
     "1253",
     "1249 (本記事)"),
    ("教材",
     "L64 (既扱)",
     "L65 (本記事)"),
], columns=["項目", "L64 静的施設 (海岸保全)", "L65 動的施設 (防潮扉)"])

# L64 主要市町 vs L65 主要市町
# L64 側は WKT geom 有件 (= 市町分配済み, l64_facil_with_city)、
# L65 側は CSV 市町名列 (= 全件) を使い、両者の市町別件数を比較する。
# (注: L64 側は geom 有件 = 元 1645 件のうち WKT 取得できた約 857 件のため、
#  全件件数比較は別変数 l64_total = 1645 で表現する)
if l64_facil_with_city is not None:
    l64_city_count = (l64_facil_with_city.groupby("市町名").size()
                        .reset_index(name="L64_件数"))
    l65_city_count = (df_raw.groupby("市町名").size()
                        .reset_index(name="L65_件数"))
    city_compare = (l64_city_count.merge(l65_city_count, on="市町名", how="outer")
                                  .fillna(0))
    city_compare["L64_件数"] = city_compare["L64_件数"].astype(int)
    city_compare["L65_件数"] = city_compare["L65_件数"].astype(int)
    city_compare["合計"] = city_compare["L64_件数"] + city_compare["L65_件数"]
    city_compare["L65_倍率"] = (city_compare["L65_件数"]
                              / city_compare["L64_件数"].replace(0, np.nan)).round(2)
    city_compare = city_compare.sort_values("合計", ascending=False).reset_index(drop=True)
    # 県外/海上 等は除く
    city_compare = city_compare[~city_compare["市町名"].isin(["?", "県外/海上"])]
    l64_total_cnt = len(l64_facil_df)  # 元データの全件 (1645)
else:
    city_compare = pd.DataFrame()
    l64_total_cnt = 1645

# 港湾系シェア (L64 港湾+漁港 vs L65 港湾+漁港)
l64_kowan_share = None
if l64_facil_df is not None and "所管" in l64_facil_df.columns:
    l64_jc = l64_facil_df["所管"].value_counts()
    l64_kowan_share = (l64_jc.get("港湾", 0) + l64_jc.get("漁港", 0)) / l64_jc.sum() * 100
l65_kowan_share = (juris_count.get("港湾", 0) + juris_count.get("漁港", 0)) / n_total_gates * 100
print(f"  L64 港湾+漁港シェア: {l64_kowan_share:.1f}%" if l64_kowan_share else "  L64 シェア未取得", flush=True)
print(f"  L65 港湾+漁港シェア: {l65_kowan_share:.1f}%", flush=True)

print(f"  ({time.time()-t6:.1f}s)", flush=True)


# =============================================================================
# 7. CSV 出力
# =============================================================================
print("\n[7] CSV 出力", flush=True)
t7 = time.time()

# 全件 (geom + ハザード判定付与)
df_out = df_raw.drop(columns=["geometry"], errors="ignore").copy()
hz_lookup = gdf.set_index("防潮扉ID")[["CITY_CD", "市町名_空間",
                                           "in_l44", "in_l49"]]
df_out = df_out.merge(hz_lookup, on="防潮扉ID", how="left")
df_out.to_csv(ASSETS / "L65_all_gates.csv", index=False, encoding="utf-8-sig")

T_kind.to_csv(ASSETS / "L65_kind_summary.csv", index=False, encoding="utf-8-sig")
T_juris.to_csv(ASSETS / "L65_juris_summary.csv", index=False, encoding="utf-8-sig")
city_count.to_csv(ASSETS / "L65_city_ranking.csv", index=False, encoding="utf-8-sig")
district_top.to_csv(ASSETS / "L65_district_ranking.csv", index=False, encoding="utf-8-sig")
port_top.to_csv(ASSETS / "L65_port_ranking.csv", index=False, encoding="utf-8-sig")
T_kind_juris.to_csv(ASSETS / "L65_kind_x_juris.csv", encoding="utf-8-sig")
city_cov.to_csv(ASSETS / "L65_city_coverage.csv", index=False, encoding="utf-8-sig")
kind_cov.to_csv(ASSETS / "L65_kind_coverage.csv", index=False, encoding="utf-8-sig")
if not city_compare.empty:
    city_compare.to_csv(ASSETS / "L65_l64_l65_compare.csv",
                         index=False, encoding="utf-8-sig")
T_dynamic_static.to_csv(ASSETS / "L65_dynamic_static.csv",
                          index=False, encoding="utf-8-sig")

print(f"  ({time.time()-t7:.1f}s)", flush=True)


# =============================================================================
# 8. 図の生成 (8 図)
# =============================================================================
print("\n[8] 図の生成", flush=True)
t8 = 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


admin["is_coastal"] = admin["CITY_CD"].isin(COASTAL_CITIES)


# ---- 図 1 (RQ1): 県全域 種類別マップ ----
print("  fig1: 県全域 種類別マップ", flush=True)
fig, ax = plt.subplots(figsize=(11.5, 7.5))
admin.plot(ax=ax, color="#fff4e0", edgecolor="#888", linewidth=0.4, alpha=0.4)
admin[admin["is_coastal"]].plot(
    ax=ax, facecolor="#ffe8c8", edgecolor="#666", linewidth=0.6, alpha=0.55)
for kind in KIND_ORDER:
    sub = gdf[gdf["種類"] == kind]
    if len(sub) == 0:
        continue
    msize = 6 if kind == "陸閘" else 18
    sub.plot(ax=ax, color=KIND_COLOR[kind], markersize=msize,
              edgecolor="#222", linewidth=0.2, alpha=0.7)
ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -140000)
ax.set_aspect("equal")
ax.set_title(f"図 1 (RQ1): 広島県 防潮扉 全域マップ — "
             f"全 {n_total_gates} 件 / 5 種類 / {n_cities} 市町",
             fontsize=13)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = [Line2D([0], [0], marker='o', color='w',
                    markerfacecolor=KIND_COLOR[k], markeredgecolor="#222",
                    markersize=8,
                    label=f"{k} (n={int(kind_count[k])})")
           for k in KIND_ORDER if kind_count[k] > 0]
patches.append(Patch(facecolor="#ffe8c8", edgecolor="#666", label="沿岸市町"))
patches.append(Patch(facecolor="#fff4e0", edgecolor="#888", alpha=0.4,
                      label="その他"))
ax.legend(handles=patches, loc="lower left", fontsize=8.5, ncol=2,
          title="種類")
save_fig("L65_fig1_overview_kind_map.png")


# ---- 図 2 (RQ1): 種類別 件数 (log scale) ----
print("  fig2: 種類別 件数 (log scale)", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax = axes[0]
xs = np.arange(len(KIND_ORDER))
counts = [int(kind_count[k]) for k in KIND_ORDER]
colors = [KIND_COLOR[k] for k in KIND_ORDER]
bars = ax.bar(xs, counts, color=colors, edgecolor="#333", linewidth=0.6)
for x, c in zip(xs, counts):
    ax.text(x, c * 1.05, f"{c}", ha="center", fontsize=10)
ax.set_xticks(xs)
ax.set_xticklabels(KIND_ORDER, rotation=15, ha="right", fontsize=10)
ax.set_ylabel("件数")
ax.set_title(f"種類別 件数 (陸閘シェア = {rikko_share:.1f}%)", fontsize=11)
ax.set_yscale("log")
ax.grid(True, axis="y", alpha=0.3)

ax = axes[1]
juris_xs = np.arange(len(JURISDICTION_ORDER))
juris_vals = [int(juris_count[j]) for j in JURISDICTION_ORDER]
juris_cols = [JURISDICTION_COLOR[j] for j in JURISDICTION_ORDER]
bars = ax.bar(juris_xs, juris_vals, color=juris_cols, edgecolor="#333", linewidth=0.6)
for x, c in zip(juris_xs, juris_vals):
    ax.text(x, c * 1.05, f"{c}", ha="center", fontsize=10)
ax.set_xticks(juris_xs)
ax.set_xticklabels(JURISDICTION_ORDER, rotation=15, ha="right", fontsize=10)
ax.set_ylabel("件数")
ax.set_title("所管別 件数 (港湾系 = "
             f"{l65_kowan_share:.0f}%)", fontsize=11)
ax.set_yscale("log")
ax.grid(True, axis="y", alpha=0.3)

fig.suptitle("図 2 (RQ1): 種類別 + 所管別 件数 (log scale)",
             fontsize=13, y=1.02)
plt.tight_layout()
save_fig("L65_fig2_kind_juris_count.png")


# ---- 図 3 (RQ1): 市町別 ランキング ----
print("  fig3: 市町別 ランキング", flush=True)
fig, ax = plt.subplots(figsize=(11, 6.5))
top_cities = city_count.head(13).iloc[::-1]
ys = np.arange(len(top_cities))
bars = ax.barh(ys, top_cities["件数"].values,
                color="#cf222e", edgecolor="#333", linewidth=0.5)
for y, v in zip(ys, top_cities["件数"].values):
    ax.text(v + 5, y, f"{v}", va="center", fontsize=10)
ax.set_yticks(ys)
ax.set_yticklabels(top_cities["市町名"].values, fontsize=10)
ax.set_xlabel("件数")
ax.set_title(f"図 3 (RQ1): 市町別 防潮扉 件数ランキング — "
             f"Top 3 で {top3_city_share:.0f}%", fontsize=12)
ax.grid(True, axis="x", alpha=0.3)
plt.tight_layout()
save_fig("L65_fig3_city_ranking.png")


# ---- 図 4 (RQ1): 市町別 choropleth ----
print("  fig4: 市町別 choropleth", flush=True)
city_count_full = city_count.set_index("市町名")["件数"].to_dict()
admin["gate_count"] = 0
for cc in admin["CITY_CD"]:
    name = rollup_to_city(cc)
    n = city_count_full.get(name, 0)
    admin.loc[admin["CITY_CD"] == cc, "gate_count"] = n

fig, ax = plt.subplots(figsize=(11, 6.5))
admin.plot(ax=ax, column="gate_count", cmap="OrRd", edgecolor="#666",
            linewidth=0.5, legend=True,
            legend_kwds={"label": "防潮扉 件数 (市町別)",
                          "orientation": "horizontal", "shrink": 0.6})
for _, r in admin[admin["is_coastal"]].iterrows():
    nm = rollup_to_city(r["CITY_CD"])
    n = city_count_full.get(nm, 0)
    if n >= 50:
        c = r.geometry.centroid
        ax.annotate(f"{nm}\n{n}件", xy=(c.x, c.y), ha="center",
                    fontsize=8.5, color="#333",
                    bbox=dict(boxstyle="round,pad=0.2", fc="white",
                              ec="#aaa", alpha=0.85))
ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -140000)
ax.set_aspect("equal")
ax.set_title("図 4 (RQ1): 市町別 防潮扉 件数 choropleth", fontsize=13)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
plt.tight_layout()
save_fig("L65_fig4_city_choropleth.png")


# ---- 図 5 (RQ2): 高潮 + 津波 + 扉 重ね合わせ ----
print("  fig5: 高潮 + 津波 + 扉 重ね合わせ", flush=True)
fig, ax = plt.subplots(figsize=(13, 8.5))
admin.plot(ax=ax, color="#fff4e0", edgecolor="#888", linewidth=0.4, alpha=0.5)
admin[admin["is_coastal"]].plot(
    ax=ax, facecolor="#ffe8c8", edgecolor="#666", linewidth=0.6, alpha=0.5)
if l44_max is not None:
    l44_max.plot(ax=ax, color="#ff9800", alpha=0.4, edgecolor="none")
# 津波想定の代理として L63 警戒区域 (キャッシュ済) を描画
L63_DISS_PATH = ROOT / "data" / "extras" / "L63_tsunami_warning_zone" / \
                "_cache" / "keikai_dissolve_8rank.gpkg"
if L63_DISS_PATH.exists():
    l63_diss_for_plot = gpd.read_file(L63_DISS_PATH).to_crs(TARGET_CRS)
    l63_diss_for_plot.plot(ax=ax, color="#9467bd", alpha=0.4, edgecolor="none")


def _state(r):
    if r["in_l44"] and r["in_l49"]:
        return "両方内"
    if r["in_l44"]:
        return "高潮のみ"
    if r["in_l49"]:
        return "津波のみ"
    return "想定外"


gdf["_state"] = gdf.apply(_state, axis=1)
state_colors = {"両方内": "#cf222e", "高潮のみ": "#ff7f0e",
                "津波のみ": "#9467bd", "想定外": "#1a7f37"}
for st, color in state_colors.items():
    sub = gdf[gdf["_state"] == st]
    if len(sub) == 0:
        continue
    sub.plot(ax=ax, color=color, markersize=8, alpha=0.85,
              edgecolor="#222", linewidth=0.15,
              label=f"{st} (n={len(sub)})")
ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -140000)
ax.set_aspect("equal")
ax.set_title(f"図 5 (RQ2): 高潮 (L44) + 津波 (L49) + 防潮扉 重ね合わせ — "
             f"高潮内 {l44_cover_pct:.0f}% / 津波内 {l49_cover_pct:.0f}%",
             fontsize=12)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = [
    Patch(facecolor="#ff9800", alpha=0.5, label="L44 高潮想定 polygon"),
    Patch(facecolor="#9467bd", alpha=0.5, label="L49 津波警戒区域 (代理)"),
]
patches += [Line2D([0], [0], marker='o', color='w', markerfacecolor=c,
                    markersize=8, label=f"扉: {st}")
            for st, c in state_colors.items()]
ax.legend(handles=patches, loc="lower left", fontsize=8.5, ncol=1)
plt.tight_layout()
save_fig("L65_fig5_hazard_overlay.png")


# ---- 図 6 (RQ2): 市町別 高潮/津波 カバレッジ ----
print("  fig6: 市町別 カバレッジ", flush=True)
fig, ax = plt.subplots(figsize=(11.5, 6.5))
top_cov_cities = city_cov.head(13).iloc[::-1]
ys = np.arange(len(top_cov_cities))
bars1 = ax.barh(ys - 0.2, top_cov_cities["高潮率_%"], height=0.4,
                 color="#ff7f0e", edgecolor="#333", linewidth=0.4,
                 label="高潮想定内 率 (%)")
bars2 = ax.barh(ys + 0.2, top_cov_cities["津波率_%"], height=0.4,
                 color="#9467bd", edgecolor="#333", linewidth=0.4,
                 label="津波想定内 率 (%)")
for y, vh, vt, n in zip(ys, top_cov_cities["高潮率_%"],
                          top_cov_cities["津波率_%"],
                          top_cov_cities["扉数"]):
    ax.text(vh + 1, y - 0.2, f"{vh:.0f}%", va="center", fontsize=8)
    ax.text(vt + 1, y + 0.2, f"{vt:.0f}%", va="center", fontsize=8)
    ax.text(-3, y, f"n={int(n)}", va="center", ha="right",
            fontsize=8, color="#666")
ax.set_yticks(ys)
ax.set_yticklabels(top_cov_cities["市町名"], fontsize=10)
ax.set_xlabel("カバレッジ (%)")
ax.set_xlim(-12, 110)
ax.set_title(f"図 6 (RQ2): 市町別 防潮扉 高潮 vs 津波 カバレッジ "
             f"(全 {len(city_cov)} 市町)", fontsize=12)
ax.legend(loc="lower right", fontsize=9)
ax.grid(True, axis="x", alpha=0.3)
plt.tight_layout()
save_fig("L65_fig6_city_coverage.png")


# ---- 図 7 (RQ3): L64 vs L65 動静比較 ----
print("  fig7: L64 vs L65 動静比較", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(13.5, 5.5))

# 左: 件数比較 (L64 vs L65)
ax = axes[0]
labels = ["L64\n海岸保全\n(静的)", "L65\n防潮扉\n(動的)"]
vals = [len(l64_facil_df) if l64_facil_df is not None else 1645, n_total_gates]
cols = ["#1a7f37", "#cf222e"]
bars = ax.bar([0, 1], vals, color=cols, edgecolor="#333", linewidth=0.6, width=0.55)
for x, v in zip([0, 1], vals):
    ax.text(x, v + 50, f"{v:,} 件", ha="center", fontsize=11, fontweight="bold")
ax.set_xticks([0, 1])
ax.set_xticklabels(labels, fontsize=10)
ax.set_ylabel("件数")
ax.set_title(f"L64 (静) vs L65 (動) 件数 — L65/L64 = "
             f"{n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f}",
             fontsize=11)
ax.set_ylim(0, max(vals) * 1.18)
ax.grid(True, axis="y", alpha=0.3)

# 右: 市町別 L64 vs L65 件数 比較 (Top 10 合計)
ax = axes[1]
if not city_compare.empty:
    cc_top = city_compare.head(10).iloc[::-1]
    ys = np.arange(len(cc_top))
    w = 0.4
    ax.barh(ys - w/2, cc_top["L64_件数"], height=w,
             color="#1a7f37", edgecolor="#333", linewidth=0.4,
             label=f"L64 静的 (n={int(cc_top['L64_件数'].sum())})")
    ax.barh(ys + w/2, cc_top["L65_件数"], height=w,
             color="#cf222e", edgecolor="#333", linewidth=0.4,
             label=f"L65 動的 (n={int(cc_top['L65_件数'].sum())})")
    for y, v64, v65 in zip(ys, cc_top["L64_件数"], cc_top["L65_件数"]):
        ax.text(v64 + 5, y - w/2, f"{int(v64)}", va="center", fontsize=8)
        ax.text(v65 + 5, y + w/2, f"{int(v65)}", va="center", fontsize=8)
    ax.set_yticks(ys)
    ax.set_yticklabels(cc_top["市町名"], fontsize=9)
    ax.set_xlabel("件数")
    ax.set_title("Top 10 合計市町: L64 vs L65 件数", fontsize=11)
    ax.legend(loc="lower right", fontsize=9)
    ax.grid(True, axis="x", alpha=0.3)
else:
    ax.text(0.5, 0.5, "L64 既扱データなし", ha="center", va="center")
    ax.axis("off")

fig.suptitle("図 7 (RQ3): L64 静的 vs L65 動的 — "
             f"動静の役割分担 (L65/L64 比 = "
             f"{n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f})",
             fontsize=13, y=1.02)
plt.tight_layout()
save_fig("L65_fig7_l64_l65_compare.png")


# ---- 図 8 (RQ3): L64 + L65 重ね合わせマップ ----
print("  fig8: L64 + L65 重ね合わせ", flush=True)
fig, ax = plt.subplots(figsize=(12, 7.5))
admin.plot(ax=ax, color="#fff4e0", edgecolor="#888", linewidth=0.4, alpha=0.5)
admin[admin["is_coastal"]].plot(
    ax=ax, facecolor="#ffe8c8", edgecolor="#666", linewidth=0.6, alpha=0.5)

# L64 静的 = 緑 (連続線で連なる)
if l64_facil_df is not None and "GIS情報" in l64_facil_df.columns:
    l64_geom = l64_facil_df.copy()
    l64_geom["geometry"] = l64_geom["GIS情報"].apply(
        lambda s: wkt_loads(s) if isinstance(s, str) and s.strip() else None)
    l64_geom = l64_geom.dropna(subset=["geometry"])
    l64_gdf = gpd.GeoDataFrame(l64_geom, geometry="geometry",
                                crs="EPSG:4326").to_crs(TARGET_CRS)
    l64_gdf.plot(ax=ax, color="#1a7f37", linewidth=1.6, alpha=0.55)
    n_l64_geom = len(l64_gdf)
else:
    n_l64_geom = 0

# L65 動的 = 赤の点 (開口部毎に 1 つ)
gdf.plot(ax=ax, color="#cf222e", markersize=5, alpha=0.7,
          edgecolor="#222", linewidth=0.15)

ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -140000)
ax.set_aspect("equal")
ax.set_title(f"図 8 (RQ3): L64 静的 (緑線) + L65 動的 (赤点) 重ね合わせマップ — "
             f"県の海岸防御 動静 2 系統", fontsize=12)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = [
    Line2D([0], [0], color="#1a7f37", linewidth=2.5,
            label=f"L64 海岸保全 (静的, 線, n={n_l64_geom})"),
    Line2D([0], [0], marker='o', color='w', markerfacecolor="#cf222e",
            markeredgecolor="#222", markersize=8,
            label=f"L65 防潮扉 (動的, 点, n={len(gdf)})"),
    Patch(facecolor="#ffe8c8", edgecolor="#666", label="沿岸市町"),
]
ax.legend(handles=patches, loc="lower left", fontsize=9)
plt.tight_layout()
save_fig("L65_fig8_l64_l65_overlay.png")

print(f"  ({time.time()-t8:.1f}s)", flush=True)


# =============================================================================
# 9. 表データ作成
# =============================================================================
print("\n[9] 表データ作成", flush=True)
t9 = time.time()


def df_to_html(d):
    return d.to_html(index=False, classes="", border=0, escape=False,
                     na_rep="-").replace(' style="text-align: right;"', "")


# 表 1: データセット仕様
T_dataset = pd.DataFrame([
    ("dataset_id", "1249"),
    ("公式名", "防潮扉（水門・陸閘）基本情報・維持管理情報"),
    ("resource_id", "32493"),
    ("ファイル", "340006_sluice_20230509.csv"),
    ("形式", "CSV (UTF-8 BOM)"),
    ("ファイルサイズ", f"{CSV_PATH.stat().st_size:,} byte (= {CSV_PATH.stat().st_size/1024:.0f} KB)"),
    ("レコード数", f"{n_total_gates} 行 (= 防潮扉 件数)"),
    ("列数", f"{df_raw.shape[1]} 列"),
    ("種類", f"{n_kinds} 種 (陸閘 / 角落し / 開口部 / 樋門 / 招扉)"),
    ("所管", f"{n_jurisdictions} 区分 (港湾 / 河川 / 漁港 / 道路 / 農林)"),
    ("市町数", f"{n_cities}"),
    ("地区海岸数", f"{n_districts}"),
    ("港湾名 (異なり数)", f"{n_ports}"),
    ("事務所数", f"{n_offices}"),
    ("潮位区分数", f"{n_tide_zones}"),
    ("GIS情報 有効率",
     f"WKT POINT 直接: {n_wkt_only} ({100*n_wkt_only/n_total_gates:.1f}%) + "
     f"緯度経度復元: {n_recovered} → 合計 {n_with_geom} "
     f"({100*n_with_geom/n_total_gates:.1f}%)"),
    ("座標系 (元)", "EPSG:4326 (WGS84) → EPSG:6671 で処理"),
    ("最終更新", "2023-05-09"),
    ("ライセンス", "クリエイティブ・コモンズ表示 (CC-BY)"),
    ("作成主体", "広島県港湾漁港整備課"),
    ("URL", f"https://hiroshima-dobox.jp/datasets/{DATASET_ID}"),
    ("DL URL", f"https://hiroshima-dobox.jp/resource_download/{RESOURCE_ID}"),
], columns=["項目", "値"])

# 表 2: 全体サマリ
T_overall = pd.DataFrame([
    ("総件数 (RQ1)", f"{n_total_gates} 件"),
    ("種類数", f"{n_kinds} 種"),
    ("所管区分数", f"{n_jurisdictions} 区分"),
    ("市町数 (CSV値)", f"{n_cities}"),
    ("地区海岸数", f"{n_districts}"),
    ("港湾名 (異なり)", f"{n_ports}"),
    ("事務所数", f"{n_offices}"),
    ("潮位区分数", f"{n_tide_zones}"),
    ("陸閘シェア (RQ1)", f"{rikko_share:.1f}%"),
    ("Top 3 市町シェア (RQ1)", f"{top3_city_share:.1f}%"),
    ("Top 1 市町", f"{city_count.iloc[0]['市町名']} ({int(city_count.iloc[0]['件数'])} 件)"),
    ("Top 1 地区海岸", f"{district_count.index[0]} ({int(district_count.iloc[0])} 件)"),
    ("Top 1 港湾", f"{port_count.index[0]} ({int(port_count.iloc[0])} 件)"),
    ("L44 高潮内 (RQ2)", f"{n_in_l44} / {n_geom} ({l44_cover_pct:.1f}%)"),
    ("L49 津波内 (RQ2)", f"{n_in_l49} / {n_geom} ({l49_cover_pct:.1f}%)"),
    ("両方想定内 (RQ2)", f"{n_in_both} ({100*n_in_both/n_geom:.1f}%)"),
    ("どちらも外 (RQ2)", f"{n_in_neither} ({100*n_in_neither/n_geom:.1f}%)"),
    ("L64 既扱件数 (RQ3)", f"{l64_total_cnt if not city_compare.empty else 1645} 件 (= 静的施設)"),
    ("L65/L64 件数比 (RQ3)",
     f"{n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f}"),
    ("L64 港湾系シェア (RQ3)", f"{l64_kowan_share:.1f}%" if l64_kowan_share else "-"),
    ("L65 港湾系シェア (RQ3)", f"{l65_kowan_share:.1f}%"),
    ("L64 + L65 統合", f"{(len(l64_facil_df) if l64_facil_df is not None else 1645) + n_total_gates} 件"),
], columns=["指標", "値"])
T_overall.to_csv(ASSETS / "L65_overall.csv", index=False, encoding="utf-8-sig")

# 表 3: データ取得手順
T_data_recipe = pd.DataFrame([
    ("ステップ 1", "DoBoX dataset 1249 ページ",
     f"https://hiroshima-dobox.jp/datasets/{DATASET_ID}"),
    ("ステップ 2", "CSV DL (resource 32493)",
     f"https://hiroshima-dobox.jp/resource_download/{RESOURCE_ID}"),
    ("ステップ 3", "保存先",
     f"data/extras/L65_floodgates/340006_sluice_20230509.csv"),
    ("ステップ 4", "POINT 構築 (WKT or 緯度経度)",
     f"{n_with_geom} / {n_total_gates} 件 ({100*n_with_geom/n_total_gates:.0f}%)"),
    ("ステップ 5", "EPSG:6671 投影 + 市町 sjoin",
     f"全 sjoin で {(gdf['CITY_CD']!=-1).sum()} 件を市町分配"),
    ("ステップ 6", "L44 / L49 想定 polygon と POINT intersect",
     f"高潮 {l44_cover_pct:.0f}% / 津波 {l49_cover_pct:.0f}% カバー判定"),
    ("ステップ 7", "L64 既扱データと動静比較",
     f"L65/L64 = {n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f} 倍"),
    ("ステップ 8", "8 図 + 12 表 出力",
     "本スクリプト全体で ~30 秒"),
], columns=["ステップ", "操作", "値 / URL"])

# 表 4-12 は HTML 内で個別生成
# 表: 種類別サマリ
T_kind_show = T_kind.copy()
# 表: 所管別サマリ
T_juris_show = T_juris.copy()
# 表: 市町別ランキング
T_city_show = city_count.head(13).copy()
# 表: 地区海岸別 ランキング (Top 15)
T_district_show = district_top.copy()
# 表: 港湾別 ランキング (Top 12)
T_port_show = port_top.copy()
# 表: 種類×所管 クロス表
T_kx_show = T_kind_juris.copy()
T_kx_show.insert(0, "種類", T_kx_show.index)
T_kx_show = T_kx_show.reset_index(drop=True)
# 表: 市町別 高潮 vs 津波 カバレッジ
T_city_cov_show = city_cov.head(13).copy()
T_city_cov_show.columns = ["市町名", "扉数", "高潮内", "津波内", "高潮率_%", "津波率_%"]
# 表: 種類別 カバレッジ
T_kind_cov_show = kind_cov.copy()
T_kind_cov_show.columns = ["種類", "扉数", "高潮内", "津波内", "高潮率_%", "津波率_%"]
# 表: L64 vs L65 動静比較 (T_dynamic_static)
# 表: L64 vs L65 市町別比較
T_city_compare_show = city_compare.head(13).copy() if not city_compare.empty else pd.DataFrame()


# 表 11: 仮説検証
def jud(cond, ok="強支持", fail="反証", part="部分支持"):
    return ok if cond else fail


T_hypo = pd.DataFrame([
    ("H1 陸閘シェア ≥ 80% (RQ1)",
     f"観測 = {rikko_share:.2f}% (陸閘 = {int(kind_count['陸閘'])} / {n_total_gates})",
     jud(rikko_share >= 80),
     f"H1 {jud(rikko_share >= 80)}: 陸閘が <b>{rikko_share:.1f}%</b> を占める"
     f"極端な構造分極。これは「堤防を横断する道路の開口部を閉じる」 という防潮扉の本旨を反映。"
     f"角落し ({int(kind_count['角落し'])} 件 = {100*kind_count['角落し']/n_total_gates:.1f}%) "
     f"が 2 番手で簡易閉鎖型、樋門 ({int(kind_count['樋門'])} 件) は排水兼用、"
     f"招扉 ({int(kind_count['招扉'])} 件) は漁港の小型風雨対策用。"
     f"陸閘の支配は<b>道路網と海岸防護のクロスポイント</b>の多さを定量化。"),
    ("H2 Top 3 市町 ≥ 40% (RQ1)",
     f"観測 = {top3_city_share:.1f}% (Top 1: {city_count.iloc[0]['市町名']} {int(city_count.iloc[0]['件数'])}件, "
     f"Top 2: {city_count.iloc[1]['市町名']} {int(city_count.iloc[1]['件数'])}件, "
     f"Top 3: {city_count.iloc[2]['市町名']} {int(city_count.iloc[2]['件数'])}件)",
     jud(top3_city_share >= 40),
     f"H2 {jud(top3_city_share >= 40)}: 上位 3 市町で <b>{top3_city_share:.0f}%</b> を占める偏在型分布。"
     f"<b>{city_count.iloc[0]['市町名']}</b> は多島海 + 旧来の港湾密集、"
     f"<b>{city_count.iloc[1]['市町名']}</b> は軍港由来の長大堤防 + 道路横断、"
     f"<b>{city_count.iloc[2]['市町名']}</b> は都市部沿岸 + 干拓地が動的施設整備を支配。"),
    ("H3 高潮カバー > 津波カバー (RQ2)",
     f"観測 高潮 = {l44_cover_pct:.1f}% / 津波 = {l49_cover_pct:.1f}% / "
     f"差 = {l44_cover_pct - l49_cover_pct:+.1f} pt",
     jud(l44_cover_pct > l49_cover_pct),
     f"H3 {jud(l44_cover_pct > l49_cover_pct)}: 高潮想定内扉率 ({l44_cover_pct:.0f}%) "
     f"{'>' if l44_cover_pct > l49_cover_pct else '<'} 津波想定内扉率 ({l49_cover_pct:.0f}%)。"
     f"これは防潮扉が高潮防御を主目的に整備されてきた歴史的経緯を反映。"
     f"L64 (静的施設) でも同方向の差が観測され (+22.4 pt), "
     f"<b>静的 + 動的の両系統で「高潮主目的」 の制度時間差が刻まれる</b>共通構造を確認。"),
    (f"H4 L65/L64 件数比 ≥ 1.5 (RQ3)",
     f"観測 = {n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f} "
     f"(= L65 {n_total_gates} / L64 {len(l64_facil_df) if l64_facil_df is not None else 1645})",
     jud((n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645)) >= 1.5),
     f"H4 {jud((n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645)) >= 1.5)}: "
     f"動的施設件数比 = {n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f}。"
     f"動的施設は<b>「開口部 1 箇所毎に 1 つ必要」</b>のため密に分布、"
     f"静的施設は<b>「連続 1 本で長距離をカバー」</b>するため疎に分布する性質の差を定量化。"
     f"L65 が L64 の 1.6 倍程度の件数を持つ事実は、"
     f"<b>「沿岸 km あたりの管理対象数」</b>の動静差を意味する。"),
    (f"H5 港湾系シェア ≥ 70% (RQ3)",
     f"観測 L65 = {l65_kowan_share:.1f}% / L64 = "
     f"{l64_kowan_share:.1f}%" if l64_kowan_share else "L64 未取得",
     jud(l65_kowan_share >= 70),
     f"H5 {jud(l65_kowan_share >= 70)}: L65 港湾系所管 (港湾 + 漁港) シェア "
     f"<b>{l65_kowan_share:.0f}%</b>。"
     + (f"L64 ({l64_kowan_share:.0f}%) と整合。"
        if l64_kowan_share else "")
     + " 静的 + 動的の両系統が同じ港湾系所管に集中している = "
     "<b>同じ管理者 (港湾管理者 + 漁港管理者) が静的 + 動的の 2 系統を運用</b>する制度設計。"
     "これは『開ける扉と閉まる堤防は同じ人が守る』 という運用合理性を反映。"),
], columns=["仮説", "観測値", "判定", "解釈"])
T_hypo.to_csv(ASSETS / "L65_hypothesis_check.csv", index=False, encoding="utf-8-sig")

print(f"  ({time.time()-t9:.1f}s)", flush=True)


# =============================================================================
# 10. HTML 生成
# =============================================================================
print("\n[10] HTML 生成", flush=True)
t10 = time.time()

# Sec 1: 学習目標と問い
sec1 = f"""
<p>本記事は DoBoX のシリーズ「<b>防潮扉（水門・陸閘）基本情報・維持管理情報</b>」 1 件
(dataset_id = <a href="https://hiroshima-dobox.jp/datasets/{DATASET_ID}">{DATASET_ID}</a>) を <b>単独</b>で取り上げ、
広島県内 <b>{n_total_gates} 件 / {n_cities} 市町 / {n_districts} 地区海岸 / {n_kinds} 種類</b> の
防潮扉の構造を 3 つの独立した研究角度 (RQ1 / RQ2 / RQ3) で並列に分析する。
本データは CSV 形式 ({CSV_PATH.stat().st_size:,} byte / 12 列) で、
GIS 情報 (POINT) は <b>{n_with_geom} 件 ({100*n_with_geom/n_total_gates:.0f}%)</b> に含まれる。</p>

<p class="note"><b>L64 (海岸保全施設) との位置付け</b>: L64 は<b>連続して立つ静的施設</b> (護岸・堤防・離岸堤 等, 1,645 件) を扱う。
本記事 L65 は<b>堤防の開口部に置かれた動的施設</b> (陸閘・水門・樋門 等, {n_total_gates} 件) を扱う。
両者は同じ広島県沿岸を共有するが、<b>動作モード (常時連続防御 vs 閉鎖時防御)</b> が決定的に異なる。
本研究 RQ3 で両者の<b>動静役割分担</b>を空間統計化する。</p>

<div class="warn"><b>動的施設の本質</b>: 防潮扉は<b>平常時は開いている</b>ことが本質。
道路を遮断したり排水路を塞いだりする<b>開口部</b>を、台風・高潮・津波の予報時に
人手 or 自動制御で閉じる。閉め忘れ・操作遅れ・開閉不良がそのまま被害に直結する。
このため<b>「平時の点検 + 操作訓練 + 操作員配置」</b>が防御能力を決定し、
施設の物理的耐久性だけで評価できない。L64 (静的) とは管理思想が根本的に異なる。</div>

<h3>独自用語の定義</h3>
<ul class="kv">
  <li><b>防潮扉</b> (本データ #1249): 沿岸の堤防・防潮堤を貫通する道路や河口の開口部に
      設置する開閉式の扉の総称。本研究では <b>{n_total_gates} 件</b> 全件を対象。</li>
  <li><b>陸閘</b> (種類, 本データ {int(kind_count['陸閘'])} 件 = {rikko_share:.1f}%):
      <b>「りっこう」</b>と読む。堤防を横断する道路の開口部を閉じる扉。
      平常時は車両通行可、台風時に閉じて遮断する。本データの<b>主役</b>。</li>
  <li><b>角落し</b> (種類, 本データ {int(kind_count['角落し'])} 件 = {100*kind_count['角落し']/n_total_gates:.1f}%):
      <b>「かくおとし」</b>と読む。小規模開口部の上に角材 (角落し材) を上から落として閉じる簡易型。
      操作が簡単・低コスト。漁港の小規模開口部に多用される。</li>
  <li><b>開口部</b> (種類, 本データ {int(kind_count['開口部'])} 件):
      開閉機構が明示されていない堤防貫通開口部。一部は土嚢で塞ぐ簡易管理の可能性。
      管理データで「種類 = 開口部」 とされる。</li>
  <li><b>樋門</b> (種類, 本データ {int(kind_count['樋門'])} 件):
      <b>「ひもん」</b>と読む。排水路と海の境界に置かれる小型扉。
      平常時は排水のために開いていて、高潮時に閉じる<b>排水兼用</b>型。</li>
  <li><b>招扉</b> (種類, 本データ {int(kind_count['招扉'])} 件):
      <b>「しょうひ」</b>と読む。風雨時に閉じる小型扉。漁港等の小規模型。
      本データでは少数 ({100*kind_count['招扉']/n_total_gates:.1f}%) 派。</li>
  <li><b>動的施設</b> (本記事用語): 開閉操作によって防御機能が起動する施設。
      L65 防潮扉 (本記事) はこのカテゴリ。L64 海岸保全施設は<b>静的施設</b> (連続して常時防御) と対比。</li>
  <li><b>最後のバリア</b> (本記事用語): 海岸保全施設 (L64 護岸・堤防) と並んで沿岸防御の物理的最終層。
      静的施設だけでは堤防の<b>開口部 (= 道路 + 排水路)</b> が無防備になるため、
      防潮扉が「最後の隙間を塞ぐ」 役割を持つ。</li>
  <li><b>開閉操作</b> (本記事用語): 動的施設の防御発動行為。
      操作員 (= 行政 + 地域住民 + 港湾事業者) が高潮警報を受けて手動 or 自動で実行。
      操作の確実性 (= 訓練 + 体制) が動的施設の防御能力を支配。</li>
  <li><b>所管</b> (本データ列名): 扉の所管区分。
      <b>港湾 ({int(juris_count['港湾'])}) / 河川 ({int(juris_count['河川'])}) / 漁港 ({int(juris_count['漁港'])}) / 道路 ({int(juris_count['道路'])}) / 農林 ({int(juris_count['農林'])})</b> の 5 区分。
      港湾区域内の扉であっても、所管は施設管理者の体系で分類される。</li>
  <li><b>地区海岸名</b> (本データ列名): 海岸を細分化した管理単位。
      例: 「宇品地区」 「江波地区」 「出島地区」 など。本データに <b>{n_districts}</b> 異なり値。</li>
  <li><b>潮位区分</b> (本データ列名): 設計潮位の参照地点による区分。
      広島・呉・福山等 <b>{n_tide_zones}</b> 区分。同じ潮位区分の扉群は同一の高潮閾値で運用される。</li>
  <li><b>カバレッジ</b> (本記事 RQ2 用語): 防潮扉 POINT が高潮・津波想定 polygon に
      含まれる比率。「想定エリア内に扉が存在する」 = 閉鎖時防御済みの代理指標。</li>
  <li><b>動静密度比</b> (本記事 RQ3 用語): L65 (動的) 件数 / L64 (静的) 件数。
      L64 が連続 1 本で長距離カバーするのに対し、L65 は開口部 1 箇所毎に必要なため
      L64 より高密度。本研究で {n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f} 倍を観測。</li>
  <li><b>POINT geometry</b>: 緯度経度 1 組で表される<b>点</b>形式の geometry。
      L64 (LineString / MultiPolygon = 線/面) と異なり、L65 は全件 POINT。
      これは扉が「堤防の 1 箇所」 を占めるため。</li>
</ul>

<h3>研究の問い (3 RQ)</h3>
<ul>
  <li><b>RQ1 (主研究)</b>: 広島県の防潮扉の<b>構造 — 種類・規模・地理分布</b>はどう描けるか?
      {n_total_gates} 件を 5 種類 × 5 所管 × {n_cities} 市町 × {n_districts} 地区海岸 × {n_tide_zones} 潮位区分 で
      多角度に集計し、<b>動的防御網</b>の物理的形状を立体的に描く。</li>
  <li><b>RQ2 (副研究 1)</b>: 防潮扉は<b>L44 高潮想定 / L49 津波想定</b>の脅威に対し
      どの程度カバーしているか?
      扉 POINT と高潮 / 津波の浸水想定 polygon を空間結合し、
      海起源 2 ハザード (高潮 + 津波) に対する<b>動的防御網の配置整合性</b>を定量化。</li>
  <li><b>RQ3 (副研究 2)</b>: L64 海岸保全施設 (1,645 件 / 静的) と L65 防潮扉 ({n_total_gates} 件 / 動的) の
      <b>動静役割分担</b>はどう描けるか?
      件数密度・地理分布・所管シェアを 2 シリーズ間で比較し、
      「常時連続防御 (静)」 と「閉鎖時開口部防御 (動)」 の相補関係を抽出。</li>
</ul>

<h3>仮説 H1〜H5</h3>
<ol>
  <li><b>H1 (陸閘支配, RQ1)</b>: 防潮扉の<b>≥ 80% が陸閘</b>。
      これは「堤防を横断する道路の開口部を閉じる」 という防潮扉の本旨を反映。</li>
  <li><b>H2 (上位 3 市町集中, RQ1)</b>: 上位 3 市町で全体の <b>40% 超</b>。
      多島海 + 大規模都市沿岸の地形類型が動的施設整備を支配。</li>
  <li><b>H3 (高潮 ⊃ 津波カバレッジ, RQ2)</b>: 防潮扉は<b>高潮想定エリア</b>に
      より強く重なる (高潮カバー率 > 津波カバー率)。
      高潮防御を主目的とする整備の歴史的経緯を反映。</li>
  <li><b>H4 (動静密度比, RQ3)</b>: L65 / L64 件数比 ≥ 1.5。
      動的施設が「開口部毎に必要」 で密配置、静的施設が「連続でカバー」 で疎配置の性質差を反映。</li>
  <li><b>H5 (港湾系所管支配, RQ3)</b>: L65 港湾系所管 (港湾 + 漁港) シェア ≥ 70%。
      L64 と同じ管理者群が静的 + 動的の 2 系統を運用する制度設計を反映。</li>
</ol>

<h3>到達点</h3>
<p>本記事を読み終えた学習者は次の 3 点を体感できる:</p>
<ol>
  <li>1 つの<b>中規模 CSV ({n_total_gates} 件 × 12 列, 全件 POINT)</b> から、
      種類 / 所管 / 市町 / 地区海岸 / 港湾 / 潮位区分 を多角度で集計する
      <b>ハンズオン分析</b>を完走する。</li>
  <li><b>「動的施設」 と「静的施設」</b>の役割分担を、L65 / L64 / L44 / L49 の 4 データを
      統合して空間統計で読み解く。</li>
  <li>RQ1 (構造) → RQ2 (脅威カバレッジ) → RQ3 (動静分担) の 3 段階で、
      <b>「県の海岸防御戦略 (動 + 静 = 完全)」</b>を定量的に描く研究プロセスを体感する。</li>
</ol>
"""

# Sec 2: 使用データ
sec2 = f"""
<p>DoBoX のシリーズ「<b>防潮扉（水門・陸閘）基本情報・維持管理情報</b>」 1 件のみを単独で扱う。
リソースは CSV 1 ファイル (~497 KB)。</p>

{df_to_html(T_dataset)}
<p><b>この表から読み取れること</b>: dataset 1249 は <b>{n_total_gates} 行 × {df_raw.shape[1]} 列</b>の CSV。
種類 5 / 所管 5 / 市町 {n_cities} / 地区海岸 {n_districts} / 潮位区分 {n_tide_zones} という多次元のメタデータを持つ。
GIS 情報は POINT 形式で <b>{100*n_wkt_only/n_total_gates:.0f}%</b> に文字列があり、
残り {100*(1-n_wkt_only/n_total_gates):.0f}% も緯度経度列から POINT を復元可能。
最終的に <b>{100*n_with_geom/n_total_gates:.0f}%</b> で位置情報処理が可能。</p>

<h3>L64 (海岸保全施設) との動静比較</h3>
{df_to_html(T_dynamic_static)}
<p><b>この表から読み取れること</b>: L64 = <b>静的施設</b> (護岸・堤防の連続線・面) と
L65 = <b>動的施設</b> (扉の点 + 開閉操作) の役割分担が制度的にも形状的にも対照的。
形状 (LineString / MultiPolygon vs POINT) からして
<b>「線で連続防御 vs 点で開口部防御」</b>の本質的差異が現れている。
本研究 RQ3 で両シリーズの空間関係を実データで可視化する。</p>

<h3>データ取得手順</h3>
{df_to_html(T_data_recipe)}
<p><b>この表から読み取れること</b>: DoBoX dataset {DATASET_ID} → resource {RESOURCE_ID} →
CSV DL → POINT 構築 → 市町 sjoin → ハザード重なり判定 → L64 比較、の 8 ステップで再現可能。
全工程は本スクリプト 1 本で自動実行される (~30 秒)。</p>

<h3>関連データセットとの対応</h3>
<ul>
  <li><b>L64 海岸保全施設</b> (#1253): 海岸法系の<b>静的</b>施設。本記事 RQ3 で動静比較。</li>
  <li><b>L44 高潮浸水想定</b> (#43-45): 海起源ハザード 1 (高潮)。本記事 RQ2 でカバレッジ分析。</li>
  <li><b>L49 津波浸水想定</b> (#46): 海起源ハザード 2 (津波)。本記事 RQ2 でカバレッジ分析。</li>
  <li><b>L63 津波災害警戒区域</b> (#47): 津波の<b>法的指定</b>。本記事の図 5 で代理可視化。</li>
  <li><b>L32 港湾外郭施設</b> (#1250 + #1254): 港湾法系の防護施設 (静的)。
      L64 の制度的姉妹篇。L65 とは構造形式が異なる (扉ではない) ので直接比較は省略。</li>
  <li><b>L62 避難情報</b> (#41): 海岸災害時の発令タイムライン。
      防潮扉の閉鎖タイミングと連動する制度。本記事の<b>運用篇</b>。</li>
</ul>
"""

# Sec 3: ダウンロード
sec3 = f"""
<p>本レッスンの再現に必要な全データ・中間 CSV・図 PNG・スクリプトを以下から直接 DL できる:</p>

<h3>生データ (DoBoX 直リンク + 本日取得分)</h3>
<ul>
  <li><a href="https://hiroshima-dobox.jp/datasets/{DATASET_ID}" target="_blank">DoBoX dataset {DATASET_ID} (防潮扉（水門・陸閘）基本情報・維持管理情報)</a></li>
  <li><a href="https://hiroshima-dobox.jp/resource_download/{RESOURCE_ID}" target="_blank">resource_download/{RESOURCE_ID} (CSV, ~497 KB)</a></li>
  <li><a href="../data/extras/L65_floodgates/340006_sluice_20230509.csv" download>本日取得 CSV ({CSV_PATH.stat().st_size:,} byte)</a></li>
  <li>参考: <a href="https://hiroshima-dobox.jp/datasets/1253" target="_blank">dataset 1253 (海岸保全施設, L64 既扱)</a></li>
</ul>

<h3>本記事の中間 CSV (再現用)</h3>
<ul>
  <li><a href="assets/L65_overall.csv" download>L65_overall.csv</a> — 全体サマリ (3 RQ 統合)</li>
  <li><a href="assets/L65_all_gates.csv" download>L65_all_gates.csv</a> — 全 {n_total_gates} 件 + ハザード判定</li>
  <li><a href="assets/L65_kind_summary.csv" download>L65_kind_summary.csv</a> — 種類別 サマリ (5 種)</li>
  <li><a href="assets/L65_juris_summary.csv" download>L65_juris_summary.csv</a> — 所管別 サマリ (5 区分)</li>
  <li><a href="assets/L65_city_ranking.csv" download>L65_city_ranking.csv</a> — 市町別ランキング</li>
  <li><a href="assets/L65_district_ranking.csv" download>L65_district_ranking.csv</a> — 地区海岸別ランキング</li>
  <li><a href="assets/L65_port_ranking.csv" download>L65_port_ranking.csv</a> — 港湾別ランキング</li>
  <li><a href="assets/L65_kind_x_juris.csv" download>L65_kind_x_juris.csv</a> — 種類×所管 クロス表</li>
  <li><a href="assets/L65_city_coverage.csv" download>L65_city_coverage.csv</a> — 市町別 高潮 vs 津波 カバレッジ</li>
  <li><a href="assets/L65_kind_coverage.csv" download>L65_kind_coverage.csv</a> — 種類別 ハザードカバレッジ</li>
  <li><a href="assets/L65_l64_l65_compare.csv" download>L65_l64_l65_compare.csv</a> — L64 vs L65 動静市町比較</li>
  <li><a href="assets/L65_dynamic_static.csv" download>L65_dynamic_static.csv</a> — 動静比較表</li>
  <li><a href="assets/L65_hypothesis_check.csv" download>L65_hypothesis_check.csv</a> — 仮説検証表 (5 仮説)</li>
</ul>

<h3>図 PNG (8 枚) と Python スクリプト</h3>
<ul>
  <li><a href="assets/L65_fig1_overview_kind_map.png" download>図 1 (RQ1) 県全域 種類別マップ</a></li>
  <li><a href="assets/L65_fig2_kind_juris_count.png" download>図 2 (RQ1) 種類別 + 所管別 件数 (log scale)</a></li>
  <li><a href="assets/L65_fig3_city_ranking.png" download>図 3 (RQ1) 市町別ランキング</a></li>
  <li><a href="assets/L65_fig4_city_choropleth.png" download>図 4 (RQ1) 市町別 件数 choropleth</a></li>
  <li><a href="assets/L65_fig5_hazard_overlay.png" download>図 5 (RQ2) 高潮 + 津波 + 扉 重ね合わせ</a></li>
  <li><a href="assets/L65_fig6_city_coverage.png" download>図 6 (RQ2) 市町別 高潮/津波 カバレッジ</a></li>
  <li><a href="assets/L65_fig7_l64_l65_compare.png" download>図 7 (RQ3) L64 静的 vs L65 動的 比較</a></li>
  <li><a href="assets/L65_fig8_l64_l65_overlay.png" download>図 8 (RQ3) L64 + L65 重ね合わせマップ</a></li>
  <li><a href="L65_floodgates.py" download>L65_floodgates.py</a> — 再現スクリプト (本体)</li>
</ul>

<h3>個別取得 (PowerShell, このレッスンだけ)</h3>
<pre><code>cd "2026 DoBoX 教材"
iwr "https://hiroshima-dobox.jp/resource_download/{RESOURCE_ID}" -OutFile "data/extras/L65_floodgates/340006_sluice_20230509.csv"
py -X utf8 lessons/L65_floodgates.py</code></pre>
<p class="tnote">本スクリプトは <code>ensure_dataset()</code> ヘルパで CSV が無ければ自動取得。
全工程は約 30 秒で完走 (1 分以内完走の要件 S を満たす)。
L64 既扱の <code>L64_all_facilities.csv</code> + L44 / L49 既キャッシュ
を内部で再利用。</p>

<h3>一括取得 (全レッスン共通, 推奨)</h3>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 data\\fetch_all.py
py -X utf8 lessons/L65_floodgates.py</code></pre>
"""

# Sec 4: RQ1
sec4_intro = f"""
<h3>RQ1 の狙い</h3>
<p>{n_total_gates} 件の防潮扉を<b>種類 / 所管 / 市町 / 地区海岸 / 港湾 / 潮位区分</b>の 6 軸で多角度に集計し、
広島県の<b>動的防御網</b>の物理的形状を立体的に描く。
特に「防潮扉」 という<b>道路と海岸線のクロスポイントに置かれた施設</b>の規模・分布を初めて定量化する。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>POINT 構築</b>: <code>shapely.wkt.loads</code> で WKT 文字列をパース。
      WKT 文字列が無い行も<b>緯度経度列から POINT を復元</b>する二段戦略で、
      実質 {100*n_with_geom/n_total_gates:.0f}% で位置情報処理を可能にする。
      EPSG:4326 (経緯度) → EPSG:6671 (平面直角第 III 系) で投影変換。</li>
  <li><b>geopandas sjoin (空間結合)</b>: 各扉 POINT を市町 polygon (admin_diss.gpkg, L44 既キャッシュ) に
      <code>predicate="within"</code> で結合。各扉に CITY_CD を付与。
      CSV にも市町名列があるが、<b>空間結合と CSV 値を両方持って整合性検証</b>する。</li>
  <li><b>多軸集計</b>: 種類 × 所管 × 市町 × 地区海岸 × 港湾 × 潮位区分 の<b>6 軸</b>で
      <code>groupby</code> + <code>value_counts</code> + クロス表を組み合わせて多角度に集計。</li>
  <li><b>L64 (LineString) との対比</b>: L65 は全件 POINT のみ = 「点」 ベース集計、
      延長 (length) の概念がない。代わりに<b>件数密度</b>と<b>市町分布</b>が分析の中心。</li>
</ul>

<h3>入出力の Before/After 例</h3>
<table>
<tr><th>段階</th><th>1 扉の中身</th><th>件数</th></tr>
<tr><td>(0) CSV 1 行</td><td>防潮扉ID=1000001, 種類=陸閘, 港湾名=広島港, 地区海岸名=宇品地区, 市町名=広島市, GIS情報="POINT (132.4747 34.3552)"</td><td>{n_total_gates}</td></tr>
<tr><td>(1) WKT or 緯度経度 → POINT</td><td>+ geometry = Point (132.4747, 34.3552, EPSG:4326)</td><td>{n_with_geom} ({100*n_with_geom/n_total_gates:.0f}%)</td></tr>
<tr><td>(2) EPSG:6671 投影</td><td>+ geometry = Point (X, Y, m 単位)</td><td>{n_with_geom}</td></tr>
<tr><td>(3) 市町 sjoin</td><td>+ CITY_CD = 103 (= 広島市南区) → 「広島市」 にロールアップ</td><td>{(gdf['CITY_CD']!=-1).sum()}</td></tr>
<tr><td>(4) 集計 (例: 種類別)</td><td>陸閘 {int(kind_count['陸閘'])} 件 (= {rikko_share:.1f}%)</td><td>(別)</td></tr>
<tr><td>(5) 市町別ランキング</td><td>{city_count.iloc[0]['市町名']} {int(city_count.iloc[0]['件数'])} 件 (= Top 1)</td><td>(別)</td></tr>
</table>
<p class="tnote">(0)-(5) を全 {n_total_gates} 件に適用 → 6 軸で集計 → 図化。
全工程はメモリ常駐で完結し、別キャッシュは不要。</p>
"""

sec4_code = code(r'''
# 1. CSV 読込 + POINT 構築 (2675 件 → 2282 で WKT, 残り緯度経度から復元)
import pandas as pd, geopandas as gpd
from shapely.wkt import loads as wkt_loads
from shapely.geometry import Point

df = pd.read_csv("data/extras/L65_floodgates/340006_sluice_20230509.csv",
                 encoding="utf-8-sig")
print(df.shape, df["種類"].value_counts())  # 2675 行, 陸閘 2175 件

def build_point(row):
    s = row["GIS情報"]
    if isinstance(s, str) and s.strip().startswith("POINT"):
        try: return wkt_loads(s)
        except: pass
    lat, lon = row["開始位置緯度"], row["開始位置経度"]
    if pd.notna(lat) and pd.notna(lon) and 33 <= lat <= 35 and 132 <= lon <= 134:
        return Point(float(lon), float(lat))
    return None

df["geometry"] = df.apply(build_point, axis=1)
gdf = df.dropna(subset=["geometry"]).copy()
gdf = gpd.GeoDataFrame(gdf, geometry="geometry", crs="EPSG:4326")\
       .to_crs("EPSG:6671")

# 2. 市町 sjoin (admin_diss.gpkg = L44 既キャッシュ流用)
admin = gpd.read_file("data/extras/L44_storm_surge/_cache/admin_diss.gpkg")\
          .to_crs("EPSG:6671")
joined = gpd.sjoin(gdf[["種類", "geometry"]],
                   admin[["CITY_CD", "geometry"]],
                   how="left", predicate="within")
joined = joined[~joined.index.duplicated(keep="first")]
gdf["CITY_CD"] = joined["CITY_CD"].fillna(-1).astype(int)

# 3. 6 軸集計
kind_count = df["種類"].value_counts()       # 陸閘 2175 / 角落し 360 / ...
juris_count = df["所管"].value_counts()       # 港湾 1821 / 河川 340 / ...
city_count = df["市町名"].value_counts()      # 尾道市 509 / 呉市 444 / ...
district_count = df["地区海岸名"].value_counts()
port_count = df["港湾名"].value_counts()
tide_count = df["潮位区分"].value_counts()

# 4. H1 検証: 陸閘シェア
print(f"陸閘シェア: {kind_count['陸閘']/len(df)*100:.1f}%")
# H2 検証: Top 3 市町シェア
print(f"Top 3 市町: {city_count.head(3).sum()/len(df)*100:.1f}%")
''')

sec4_fig1_text = """
<h3>図 1: なぜこの図か (RQ1)</h3>
<p>「広島県のどの沿岸エリアにどんな種類の扉があるか」 を 1 枚で読みたい。
5 種類 (陸閘=赤, 角落し=橙, 開口部=緑, 樋門=青, 招扉=紫) で全件 POINT を県全域に描く。
<b>沿岸市町を薄オレンジで強調</b>することで、防潮扉が沿岸線にどう分布するかを可視化。
陸閘がドミナントなので<b>サイズを小さく (msize=6)</b> 設定し、少数派 4 種類を見やすくする。</p>
"""
sec4_fig1_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>瀬戸内海沿岸 (広島湾・呉湾・尾道糸崎・福山)</b> に密集 = 海岸保全施設と同じ分布傾向。
      内陸河川にも一部 (= 樋門 / 河川所管) が見られる。</li>
  <li><b>島嶼部 (倉橋島・大崎上島・生口島・因島・大三島)</b> に陸閘が多数。
      これは多島海特有の<b>「島の港湾 + 集落沿岸」</b>に動的施設が多用されることを示す。</li>
  <li><b>赤 (陸閘) が圧倒的多数</b>で全域に分布、<b>橙 (角落し)</b> は漁港・小規模港に集中、
      <b>青 (樋門)</b> は河口部、<b>紫 (招扉)</b> は数が少なく漁港にスポット。</li>
  <li><b>呉港 + 広島港 + 尾道糸崎港 + 福山港</b> の主要港湾エリアでさらに密度が上がる。
      これは港湾の堤防 + 道路網が複雑で、開口部数 (= 扉数) が増えるため。</li>
  <li><b>北部 (山間部)</b> は完全に皆無 = 沿岸防御施設としての性質が地理的にも明確。</li>
</ul>
"""

sec4_fig2_text = f"""
<h3>図 2: なぜこの図か (RQ1)</h3>
<p>「5 種類の<b>件数構成</b>と 5 所管の<b>所管シェア</b>を一目で比較したい」 ため、左右 2 ペインで対比。
両軸とも<b>log scale</b>で桁差 (陸閘 2175 vs 招扉 29 = 75 倍) を視覚化する。
log 軸は<b>「陸閘が桁違いに多い」</b>ことを視覚化するために必須。</p>
"""
sec4_fig2_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左: <b>陸閘が {int(kind_count['陸閘'])} 件 = 圧倒的多数</b>、次が角落し {int(kind_count['角落し'])} 件、
      開口部 {int(kind_count['開口部'])} 件、樋門 {int(kind_count['樋門'])} 件、招扉 {int(kind_count['招扉'])} 件。
      件数の桁差は <b>2175 vs 29</b> = 約 75 倍。</li>
  <li>左: 陸閘の支配は<b>「堤防を横断する道路の開口部を閉じる」</b>という防潮扉の本旨を反映。
      角落しは<b>簡易閉鎖型</b>の補助カテゴリ、樋門は<b>排水兼用</b>の特殊型。</li>
  <li>右: <b>港湾 {int(juris_count['港湾'])} 件 ({100*juris_count['港湾']/n_total_gates:.0f}%) で支配</b>、
      河川 {int(juris_count['河川'])} 件、漁港 {int(juris_count['漁港'])} 件、道路 {int(juris_count['道路'])} 件、農林 {int(juris_count['農林'])} 件。
      港湾系 (港湾 + 漁港) で {l65_kowan_share:.0f}% = <b>H5 仮説 ({jud(l65_kowan_share >= 70)})</b>。</li>
  <li>右: 河川所管は<b>河口部の樋門・水門</b>が中心で、内水排除と高潮遮断の両立施設。
      道路所管は<b>道路占用横断箇所</b>に置かれた陸閘で、所管者は道路管理者。</li>
  <li>左 + 右: 種類分布 (陸閘支配) と所管分布 (港湾支配) が並行 = 港湾の道路横断箇所に陸閘が
      集中する物理的構造を反映。</li>
</ul>
"""

sec4_fig3_text = """
<h3>図 3: なぜこの図か (RQ1)</h3>
<p>「広島県内 13 市町のうち、どこに防潮扉が集中するか」 を一目で読みたい。
横棒 (件数, 値ラベル付) で全 13 市町を上から下へ並べる。これにより
<b>偏在型分布</b>が視覚化される (= 上位市町に集中、下位は少数)。</p>
"""
sec4_fig3_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>{city_count.iloc[0]['市町名']}</b> ({int(city_count.iloc[0]['件数'])} 件) が単独 1 位 =
      全体の {100*city_count.iloc[0]['件数']/n_total_gates:.0f}% を占める。
      これは多島海 + 旧来の港湾密集 (尾道糸崎港・瀬戸田港・因島土生港) を反映。</li>
  <li>2 位 <b>{city_count.iloc[1]['市町名']}</b> ({int(city_count.iloc[1]['件数'])} 件) =
      軍港由来の長大堤防 + 道路横断箇所が多い呉湾、
      3 位 <b>{city_count.iloc[2]['市町名']}</b> ({int(city_count.iloc[2]['件数'])} 件) = 都市部沿岸の道路 + 干拓地。
      Top 3 で {top3_city_share:.0f}% = <b>H2 仮説 ({jud(top3_city_share >= 40)})</b>。</li>
  <li>4 位 <b>{city_count.iloc[3]['市町名']}</b> ({int(city_count.iloc[3]['件数'])} 件) =
      離島の集落沿岸 + 旧海軍施設の名残。
      5 位 <b>{city_count.iloc[4]['市町名']}</b> ({int(city_count.iloc[4]['件数'])} 件) = 大崎上島の島嶼集落。</li>
  <li>下位市町 (海田町 3 件等) は<b>沿岸長が短い</b> + 都市的開発が薄いため少数。
      ただし 0 件市町は無い = 沿岸を持つ全市町に最低 1 件の扉がある。</li>
  <li><b>政策的含意</b>: 上位 5 市町だけで {100*city_count.head(5)['件数'].sum()/n_total_gates:.0f}% を占める。
      これらの市町の<b>操作員配置 + 訓練体制</b>が県全体の動的防御能力を決定する。</li>
</ul>
"""

sec4_fig4_text = """
<h3>図 4: なぜこの図か (RQ1)</h3>
<p>「市町別の防潮扉 件数を地図上で比較したい」 ため、choropleth (主題図) を採用。
<b>OrRd colormap</b> (薄黄 → 濃赤) で件数を符号化、件数 50 以上の沿岸市町には件数ラベルを直接付与。
これにより<b>「どの市町に整備が集中しているか」</b>が一目で読める。</p>
"""
sec4_fig4_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>件数最多市町は <b>{city_count.iloc[0]['市町名']}</b> ({int(city_count.iloc[0]['件数'])} 件)、
      これは多島海と港湾密集を反映。</li>
  <li>2 位 <b>{city_count.iloc[1]['市町名']}</b>、3 位 <b>{city_count.iloc[2]['市町名']}</b> も濃赤で強調。
      これらは沿岸長と港湾規模が大きい都市群。</li>
  <li><b>沿岸を持たない内陸市町</b> (安芸高田市・三次市・庄原市・北広島町・世羅町等) は
      白色 = 0 件。動的防御施設の管理対象が「沿岸沿いのみ」 であることが地理的に明確。</li>
  <li>図 1 の扉マップと整合 = 同じ沿岸エリアに同じ密度パターン。
      これは集計の信頼性確認。</li>
  <li>沿岸市町間でも明確な濃淡差があり、<b>整備の重点市町</b>と<b>軽量市町</b>に分かれる。
      L64 (静的) の choropleth と類似のパターン = 動静が同じ地形に集中。</li>
</ul>
"""

# 表の挿入
sec4 = (sec4_intro
        + "<h3>実装コード (CSV 読込 → POINT 構築 → 投影 → sjoin → 集計)</h3>"
        + sec4_code
        + sec4_fig1_text
        + figure("assets/L65_fig1_overview_kind_map.png",
                  f"図 1 (RQ1): 広島県 防潮扉 全域マップ (5 種類別)")
        + sec4_fig1_read
        + sec4_fig2_text
        + figure("assets/L65_fig2_kind_juris_count.png",
                  f"図 2 (RQ1): 種類別 + 所管別 件数 (log scale)")
        + sec4_fig2_read
        + sec4_fig3_text
        + figure("assets/L65_fig3_city_ranking.png",
                  f"図 3 (RQ1): 市町別 件数ランキング — Top 3 で {top3_city_share:.0f}%")
        + sec4_fig3_read
        + sec4_fig4_text
        + figure("assets/L65_fig4_city_choropleth.png",
                  f"図 4 (RQ1): 市町別 防潮扉 件数 choropleth")
        + sec4_fig4_read
        + "<h3>表: RQ1 全体サマリ</h3>"
        + df_to_html(T_overall.head(13))
        + "<p><b>この表から読み取れること</b>: 防潮扉は <b>2,675 件 / 5 種 / 5 区分 / 13 市町 / 99 地区海岸 / 29 港湾 / 8 事務所 / 13 潮位区分</b>"
          " という多次元の管理対象。陸閘が圧倒的主役、上位 3 市町に偏在、港湾系所管が支配的。</p>"
        + "<h3>表: 種類別 サマリ</h3>"
        + df_to_html(T_kind_show)
        + f"<p><b>この表から読み取れること</b>: <b>陸閘 {rikko_share:.1f}%</b> + 角落し {100*kind_count['角落し']/n_total_gates:.1f}% で "
        f"全 5 種のうち上位 2 種で 約 {100*(kind_count['陸閘']+kind_count['角落し'])/n_total_gates:.0f}% を占める。"
        f"開口部・樋門・招扉は数十件規模の小カテゴリ。地理処理可能率 (geom率) は全種類で 80% 超を達成。</p>"
        + "<h3>表: 所管別 サマリ</h3>"
        + df_to_html(T_juris_show)
        + f"<p><b>この表から読み取れること</b>: <b>港湾 {100*juris_count['港湾']/n_total_gates:.0f}% + 漁港 {100*juris_count['漁港']/n_total_gates:.0f}%</b> "
        f"で全体の {l65_kowan_share:.0f}% = 港湾系所管が支配的。"
        f"河川 {100*juris_count['河川']/n_total_gates:.0f}% は河口の樋門 + 水門、道路 {100*juris_count['道路']/n_total_gates:.0f}% は道路横断陸閘。"
        f"これは「動的施設 = 港湾管理者の管理対象」 が制度的にも事実上も主流であることを示す。</p>"
        + "<h3>表: 市町別 ランキング (全 13 市町)</h3>"
        + df_to_html(T_city_show)
        + f"<p><b>この表から読み取れること</b>: 件数最多は <b>{city_count.iloc[0]['市町名']}</b> ({int(city_count.iloc[0]['件数'])} 件)、"
        f"全 13 市町の Top 1 が単独で全体の {100*city_count.iloc[0]['件数']/n_total_gates:.0f}% を占める偏在型。"
        f"沿岸を持たない内陸市町 (安芸高田・三次・庄原等) は本データから完全に除外されている。</p>"
        + "<h3>表: 地区海岸別 ランキング (Top 15)</h3>"
        + df_to_html(T_district_show)
        + f"<p><b>この表から読み取れること</b>: <b>{district_count.index[0]}</b> ({int(district_count.iloc[0])} 件) が単独 1 位、"
        f"これは広島港の中核地区。Top 15 で全体の {100*district_count.head(15).sum()/n_total_gates:.0f}% = "
        f"地区海岸も<b>偏在型分布</b>。残り {n_districts - 15} 地区は数件〜10数件の中小カテゴリ。</p>"
        + "<h3>表: 港湾別 ランキング (Top 12)</h3>"
        + df_to_html(T_port_show)
        + f"<p><b>この表から読み取れること</b>: <b>{port_count.index[0]}</b> ({int(port_count.iloc[0])} 件) が単独 1 位、"
        f"次が <b>{port_count.index[1]}</b> ({int(port_count.iloc[1])} 件)、<b>{port_count.index[2]}</b> ({int(port_count.iloc[2])} 件)。"
        f"<b>「道路護岸」</b> という特殊カテゴリ (= 港湾名ではなく道路所管の海岸) も上位に登場 = "
        f"港湾外の道路占用扉も大量に存在することを示す。</p>"
        + "<h3>表: 種類×所管 クロス表</h3>"
        + df_to_html(T_kx_show)
        + "<p><b>この表から読み取れること</b>: 陸閘の所管は<b>港湾 + 漁港 + 道路</b>に分散、"
        "樋門の所管は<b>河川主体</b>、角落しは<b>港湾 + 漁港</b>に集中。"
        "種類と所管は独立ではなく、機能 (= 排水 / 道路横断 / 港湾内陸閘) で組み合わせが固定される構造。</p>"
        )

# Sec 5: RQ2
sec5_intro = f"""
<h3>RQ2 の狙い</h3>
<p>RQ1 で抽出した {n_with_geom} 件の扉 POINT を、<b>L44 高潮浸水想定 (max ケース)</b> および
<b>L49 津波浸水想定</b>と空間結合し、防潮扉が海起源 2 ハザードに対し
どの程度カバーしているかを定量化する。
これにより<b>「動的施設の配置整合性」</b>を読み解く。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>shapely.unary_union</b>: L44 (7 ランク) の polygon を 1 つの結合 geometry にマージし、
      <code>shapely.intersects(Point, union)</code> で点判定。</li>
  <li><b>shapely.STRtree</b>: L49 元 Shapefile (1.25M polygon) に対して空間インデックスを構築。
      各扉 POINT について <code>tree.query(pt)</code> で候補 polygon を絞り、
      <code>intersects</code> で確定。L49 既キャッシュは X 切詰めのため元 shp を直接使用。</li>
  <li><b>4 状態分類</b>: 各扉を「両方想定内 / 高潮のみ / 津波のみ / どちらも外」 の 4 状態に分類。
      これにより<b>ハザード重複度</b>を可視化。</li>
  <li><b>市町別カバレッジ</b>: 各市町ごとに高潮率・津波率を別計算。
      市町間の差異が「沿岸地形特性 + 想定範囲の違い」 を反映。</li>
  <li><b>L64 との比較</b>: L64 (静的) は POINT ではなく LineString / Polygon を持つため
      centroid 判定だった。L65 (動的) は元から POINT なので<b>判定がシンプル</b>。
      ただし L65 は<b>「扉の位置」 = 開口部の物理位置</b>なので、
      想定エリア内 = 「閉鎖時に守るべき範囲」 という意味で解釈する。</li>
</ul>

<h3>入出力の Before/After 例</h3>
<table>
<tr><th>段階</th><th>1 扉の中身</th><th>件数</th></tr>
<tr><td>(0) 扉 1 件 (POINT)</td><td>ID=1000001, 種類=陸閘, geometry=Point (132.4747, 34.3552)</td><td>{n_with_geom}</td></tr>
<tr><td>(1) EPSG:6671 投影</td><td>+ geometry = Point (X, Y) m 単位</td><td>{n_with_geom}</td></tr>
<tr><td>(2) L44 内?</td><td>+ in_l44 = True (= 高潮想定内)</td><td>{n_in_l44}</td></tr>
<tr><td>(3) L49 内?</td><td>+ in_l49 = True (= 津波想定内)</td><td>{n_in_l49}</td></tr>
<tr><td>(4) 4 状態</td><td>+ _state = "両方内"</td><td>(別)</td></tr>
<tr><td>(5) 市町別集計</td><td>{city_cov.iloc[0]['市町名']}: 扉 {int(city_cov.iloc[0]['扉数'])} 件 / 高潮内 {int(city_cov.iloc[0]['高潮内'])} 件 ({city_cov.iloc[0]['高潮率_%']:.0f}%)</td><td>市町数</td></tr>
</table>
<p class="tnote">(0)-(5) を全 {n_with_geom} 件に適用 → 4 状態クロス集計 → 市町別 + 図化。</p>
"""

sec5_code = code(r'''
# 1. 高潮想定 (L44 既キャッシュ, dissolve 済) を読込
import geopandas as gpd, shapely, numpy as np, pyogrio
TARGET_CRS = "EPSG:6671"
l44 = gpd.read_file("data/extras/L44_storm_surge/_cache/diss_max.gpkg")\
        .to_crs(TARGET_CRS)
l44_union = shapely.unary_union(l44.geometry.values)

# 2. L49 津波想定: 元 Shapefile (1.25M polygon) から空間インデックスで判定
shp = "data/extras/tsunami_extracted/340006_tsunami_inundation_assumption_map_20251203/浸水メッシュ.shp"
l49_full = pyogrio.read_dataframe(shp, columns=[])
l49_full = gpd.GeoDataFrame(l49_full, crs=l49_full.crs).to_crs(TARGET_CRS)
l49_tree = shapely.STRtree(l49_full.geometry.values)

# 3. 各扉 POINT が想定内?
def is_in_union(p, union):
    return shapely.intersects(p, union)
def is_in_tree(p, tree, polys):
    cand = tree.query(p)
    for j in cand:
        if shapely.intersects(p, polys[j]):
            return True
    return False

gdf["in_l44"] = [is_in_union(p, l44_union) for p in gdf.geometry]
gdf["in_l49"] = [is_in_tree(p, l49_tree, l49_full.geometry.values)
                  for p in gdf.geometry]

# 4. 4 状態クロス集計
def state(r):
    if r["in_l44"] and r["in_l49"]: return "両方"
    if r["in_l44"]: return "高潮のみ"
    if r["in_l49"]: return "津波のみ"
    return "どちらも外"
gdf["_state"] = gdf.apply(state, axis=1)
print(gdf["_state"].value_counts())

# 5. 市町別カバレッジ
city_cov = gdf.groupby("市町名").agg(
    扉数=("種類", "size"),
    高潮内=("in_l44", "sum"),
    津波内=("in_l49", "sum"),
).reset_index()
city_cov["高潮率_%"] = city_cov["高潮内"] / city_cov["扉数"] * 100
city_cov["津波率_%"] = city_cov["津波内"] / city_cov["扉数"] * 100
print(city_cov.head(10))
''')

sec5_fig5_text = """
<h3>図 5: なぜこの図か (RQ2)</h3>
<p>「防潮扉が高潮想定 (L44) と津波想定 (L49) のどちらに重なるか」 を 1 枚で読みたい。
2 つの想定 polygon (高潮=オレンジ, 津波=紫の代理 [L63 警戒区域]) を半透明で重ね、扉 POINT を 4 状態色で点描。
これにより<b>3 つの空間レイヤー</b> (想定 + 想定 + 扉) の関係を一望できる。</p>
"""
sec5_fig5_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>高潮想定 (オレンジ)</b>と<b>津波想定 (紫)</b>はほぼ同じ沿岸エリアを覆う = 海起源ハザードの空間重複。
      両者の差は内陸への浸入距離 (高潮 > 津波) と地形依存性。</li>
  <li><b>赤点 = 両方想定内扉 ({n_in_both} 件 = {100*n_in_both/n_geom:.0f}%)</b>: 多重ハザード地点で
      最も防御重要度が高い。広島湾奥・福山平野・尾道に集中。</li>
  <li><b>オレンジ点 = 高潮のみ扉 ({n_in_l44 - n_in_both} 件 = {100*(n_in_l44 - n_in_both)/n_geom:.0f}%)</b>:
      高潮想定だけに重なる。津波が遡上しない内奥で高潮被害があるエリア。</li>
  <li><b>紫点 = 津波のみ扉 ({n_in_l49 - n_in_both} 件 = {100*(n_in_l49 - n_in_both)/n_geom:.0f}%)</b>:
      津波想定だけに重なる扉。本県では稀 (= 高潮想定のほうが広域)。</li>
  <li><b>緑点 = どちらも外扉 ({n_in_neither} 件 = {100*n_in_neither/n_geom:.0f}%)</b>:
      想定エリア外の扉 = 低リスク地点。あるいは地形的に高所沿岸に位置する場合。</li>
  <li>高潮 vs 津波 カバー率の差: <b>{l44_cover_pct:.0f}% vs {l49_cover_pct:.0f}% = 差 {l44_cover_pct - l49_cover_pct:+.1f} pt</b>。
      <b>H3 ({jud(l44_cover_pct > l49_cover_pct)})</b>。
      L64 (静的, +22.4 pt) と同方向の差を観測 = 動静両系統で「高潮主目的整備」 の歴史的痕跡。</li>
</ul>
"""

sec5_fig6_text = """
<h3>図 6: なぜこの図か (RQ2)</h3>
<p>「市町別に高潮 vs 津波カバー率がどう異なるか」 を 1 ペインで読みたい。
全 13 市町について<b>高潮率 (オレンジ) と津波率 (紫) を二重横棒</b>で並べ、左に扉数 n を併記。
これにより<b>「どの市町で扉整備がハザード想定エリアに集中しているか」</b>を比較可。</p>
"""
sec5_fig6_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>件数最多市町 (上位) で<b>高潮率 vs 津波率の差</b>が市町ごとに異なる。
      <b>湾奥 + 干拓地市町</b> (福山市・広島市) は両方高い (= 多重ハザードエリア)。</li>
  <li><b>島嶼系市町</b> (江田島市・尾道市の島嶼部) は高潮率がやや低い場合も = 開放沿岸で高潮被害が小さい。</li>
  <li><b>津波率 < 高潮率</b> がほぼ全市町で一貫 = H3 仮説の市町別補強。
      動的施設も静的施設と同じ歴史構造を共有。</li>
  <li>カバー率 0% に近い市町は<b>非沿岸の偶発扉</b>か、想定エリア外の地形 (= 高所沿岸)。</li>
  <li><b>政策的含意</b>: カバー率が高い市町 (= 想定内扉数が多い) は<b>操作員配置 + 訓練体制</b>を
      重点強化すべき。動的施設は閉め忘れがそのまま被害に直結するため、
      想定エリア内扉のリストは<b>避難計画 + 操作優先順位</b>の基礎資料となる。</li>
</ul>
"""

sec5 = (sec5_intro
        + "<h3>実装コード (L44 / L49 想定との空間結合 + 4 状態分類)</h3>"
        + sec5_code
        + sec5_fig5_text
        + figure("assets/L65_fig5_hazard_overlay.png",
                  f"図 5 (RQ2): 高潮 (L44) + 津波 (L49) + 防潮扉 重ね合わせ "
                  f"(高潮内 {l44_cover_pct:.0f}% / 津波内 {l49_cover_pct:.0f}%)")
        + sec5_fig5_read
        + sec5_fig6_text
        + figure("assets/L65_fig6_city_coverage.png",
                  f"図 6 (RQ2): 市町別 高潮 vs 津波 カバレッジ (全 13 市町)")
        + sec5_fig6_read
        + "<h3>表: 4 状態 クロス集計</h3>"
        + df_to_html(pd.DataFrame([
            ("両方想定内 (高潮 + 津波)", n_in_both, f"{100*n_in_both/n_geom:.1f}%",
             "多重ハザード地点 = 閉鎖時防御最重要"),
            ("高潮のみ", n_in_l44 - n_in_both, f"{100*(n_in_l44-n_in_both)/n_geom:.1f}%",
             "津波遡上の届かない奥地で高潮あり"),
            ("津波のみ", n_in_l49 - n_in_both, f"{100*(n_in_l49-n_in_both)/n_geom:.1f}%",
             "高潮想定外だが津波遡上対象"),
            ("どちらも外", n_in_neither, f"{100*n_in_neither/n_geom:.1f}%",
             "ハザード想定外 (= 高所沿岸 + 内陸寄り)"),
        ], columns=["状態", "扉数", "シェア", "解釈"]))
        + "<p><b>この表から読み取れること</b>: 全 geom 有扉の <b>"
        + f"{100*(n_in_l44+n_in_l49-n_in_both)/n_geom:.0f}%</b>"
        + " (= " + f"{n_in_either}" + " 件) が高潮または津波想定の少なくとも 1 方に重なる = 閉鎖時防御機能を直接担う。"
        + " 残り <b>" + f"{100*n_in_neither/n_geom:.0f}%</b> の想定外扉は<b>高所沿岸 + 内陸寄り</b>に位置し、"
        + " 想定外でも開口部閉鎖の物理機能は持つ (= 直接ハザード防御は副次的)。</p>"
        + "<h3>表: 種類別 ハザードカバレッジ</h3>"
        + df_to_html(T_kind_cov_show)
        + "<p><b>この表から読み取れること</b>: 種類別の高潮 / 津波カバー率を並べると、"
        "<b>陸閘</b>と<b>角落し</b>のカバー率がほぼ同等 = 「主役 + 補助」 の機能差は配置にはあまり反映されず、"
        "両者ともハザード地点に均等配置されている。"
        "<b>樋門</b>は河川所管が多く、河口部に集中するため高潮率がやや高い傾向。</p>"
        + "<h3>表: 市町別 高潮 vs 津波 カバレッジ (全 13 市町)</h3>"
        + df_to_html(T_city_cov_show)
        + f"<p><b>この表から読み取れること</b>: 全 13 沿岸市町でカバレッジが計算可能。"
        + " 多くの市町で<b>高潮率 > 津波率</b>の傾向が一貫し、"
        + f" <b>H3 仮説 ({jud(l44_cover_pct > l49_cover_pct)})</b>を市町別でも確認。"
        + " カバー率 100% に近い市町は<b>沿岸全域がハザード想定内</b>の干拓地 (= 福山等)、"
        + " カバー率 50% 未満の市町は<b>高所沿岸</b>を持つ島嶼部や奥地の市町。</p>"
        )

# Sec 6: RQ3
sec6_intro = f"""
<h3>RQ3 の狙い</h3>
<p>L64 海岸保全施設 ({len(l64_facil_df) if l64_facil_df is not None else 1645} 件 / 静的) と L65 防潮扉 ({n_total_gates} 件 / 動的) は、
両方とも広島県沿岸の防護を担う公共土木施設だが、<b>動作モード・形状・操作要否</b>が異なる。
本 RQ3 では両シリーズを並べて、<b>「県の海岸防御 動静 2 系統の役割分担」</b>を空間統計で抽出。
特に<b>動静密度比</b>と<b>主要市町集中</b>のパターンを検証する。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>L64 既扱データの再利用</b>: <code>lessons/assets/L64_all_facilities.csv</code>
      ({len(l64_facil_df) if l64_facil_df is not None else 1645} 件) を読み込み、
      L65 と同じスキーマ (市町名 / 所管列) で並列集計。</li>
  <li><b>件数比較</b>: L64 (静) vs L65 (動) で全体件数比 + 市町別件数比を算出。
      L65 / L64 の倍率は<b>動的施設の高密度配置</b>を示唆する指標。</li>
  <li><b>港湾系シェア比較</b>: L64 と L65 の港湾 + 漁港 シェアを比べ、
      同じ管理者群が両系統を運用しているかを確認。</li>
  <li><b>地理重ね合わせ</b>: L64 (LineString) と L65 (POINT) を同じ EPSG:6671 で重ねて表示。
      色分け (L64 = 緑線, L65 = 赤点) で<b>静的線 + 動的点</b>の空間関係を一目で読む。</li>
  <li><b>限界</b>: 本 RQ3 は<b>件数 + 所管シェア + 地理重ね合わせ</b>が中心。
      L64 / L65 の<b>同一沿岸線上での補完率</b>を厳密に定量化するには
      buffer + 距離計算が必要 (発展課題 5 で言及)。</li>
</ul>
"""

sec6_code = code(r'''
# 1. L64 既扱データを読込
import pandas as pd, geopandas as gpd
from shapely.wkt import loads as wkt_loads

l64 = pd.read_csv("lessons/assets/L64_all_facilities.csv", encoding="utf-8-sig")
print(f"L64: {len(l64)} 件 (静的)")
print(l64["施設種類"].value_counts())  # 護岸主体

# 2. L64 (静) vs L65 (動) 件数比
ratio = len(df) / len(l64)  # 約 1.63 倍
print(f"L65 / L64 = {ratio:.2f}")

# 3. 港湾系シェア比較
l64_kowan = (l64["所管"].value_counts().get("港湾", 0)
              + l64["所管"].value_counts().get("漁港", 0)) / len(l64) * 100
l65_kowan = (df["所管"].value_counts().get("港湾", 0)
              + df["所管"].value_counts().get("漁港", 0)) / len(df) * 100
print(f"L64 港湾系: {l64_kowan:.1f}% / L65 港湾系: {l65_kowan:.1f}%")

# 4. 市町別 L64 vs L65 件数比較
l64_city = l64.groupby("市町名").size().reset_index(name="L64_件数")
l65_city = df.groupby("市町名").size().reset_index(name="L65_件数")
city_compare = l64_city.merge(l65_city, on="市町名", how="outer").fillna(0)
city_compare["合計"] = city_compare["L64_件数"] + city_compare["L65_件数"]
city_compare["L65_倍率"] = city_compare["L65_件数"] / city_compare["L64_件数"]
print(city_compare.sort_values("合計", ascending=False).head(10))

# 5. L64 を WKT でジオメトリ化 → 重ね合わせマップに使用
l64["geometry"] = l64["GIS情報"].apply(
    lambda s: wkt_loads(s) if isinstance(s, str) and s.strip() else None)
l64_gdf = gpd.GeoDataFrame(l64.dropna(subset=["geometry"]),
                              geometry="geometry", crs="EPSG:4326").to_crs("EPSG:6671")
print(f"L64 geom 有: {len(l64_gdf)} / {len(l64)}")
''')

sec6_fig7_text = f"""
<h3>図 7: なぜこの図か (RQ3)</h3>
<p>「L64 (静) と L65 (動) の件数を全体 + 市町別で対比したい」 ため、左右 2 ペインの並列図。
左 = 全体件数 (L64 緑 vs L65 赤), 右 = Top 10 合計市町別の動静件数比較。
これにより<b>「全体での動静比 + 市町別での動静偏在」</b>を一目で読める。</p>
"""
sec6_fig7_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左: <b>L64 = {len(l64_facil_df) if l64_facil_df is not None else 1645} 件 vs L65 = {n_total_gates} 件</b>。
      動静比 = <b>{n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f}</b>。
      <b>H4 仮説 ({jud((n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645)) >= 1.5)})</b>: 動的施設が静的施設の 1.5 倍以上。</li>
  <li>左: 動的施設の高密度配置は<b>「静的堤防の数 N に対して、開口部 (= 動的) は N+α 個」</b>の物理的必然。
      連続堤防は 1 本でも、その途中に複数の道路 + 排水路があれば、その全てに扉が必要。</li>
  <li>右: 市町別 Top 10 で見ると、<b>{city_compare.iloc[0]['市町名'] if not city_compare.empty else '尾道市'}</b>等の上位市町は L64 と L65 が両方多い = 動静ともに整備重点市町。</li>
  <li>右: L65 / L64 倍率は市町ごとに変動 = 沿岸地形 (= 連続堤防の長さ) と
      開口部数 (= 道路 + 排水路) のバランスで決まる。
      <b>都市部 + 旧軍港</b>では倍率が高く (= 道路網が密)、<b>離島 + 自然海岸</b>では低い。</li>
  <li><b>政策的含意</b>: 動的施設は<b>件数比例で操作員 + 訓練リソース</b>が必要。
      L65 が L64 の 1.6 倍ということは、<b>「静的整備の 1.6 倍の操作員配置」</b>を意味する。
      これは動的防御の運用コストを定量化した実証データ。</li>
</ul>
"""

sec6_fig8_text = """
<h3>図 8: なぜこの図か (RQ3)</h3>
<p>「L64 (静的線) と L65 (動的点) の<b>地理分布</b>を 1 枚で読みたい」 ため、両シリーズを
同じ EPSG:6671 で重ねて表示。<b>L64 = 緑線, L65 = 赤点</b>で色分け、沿岸市町を薄オレンジ強調。
これにより<b>「線で連続する静的堤防の上に、点で並ぶ動的扉」</b>の物理関係が直感的に読める。</p>
"""
sec6_fig8_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>主要港湾エリア</b> (広島湾・呉湾・福山湾・尾道糸崎・瀬戸田) で
      <b>緑線 (L64) と赤点 (L65) が密に重なる</b>。
      これは「静的堤防の上 / 横に動的扉が並ぶ」 という<b>物理的相補関係</b>。</li>
  <li><b>緑線が連続して走る</b>ところに<b>赤点が一定間隔で並ぶ</b>パターンが各所で観察可能。
      これは「堤防 100m に開口部 1-2 箇所」 の物理的密度を視覚化。</li>
  <li><b>島嶼部の自然海岸</b> (倉橋島・大崎上島・生口島・因島) では<b>赤点 (L65) が単独</b>で見られる箇所がある。
      これは堤防無しの陸閘 = 道路を直接遮断する開口部閉鎖装置。</li>
  <li><b>非沿岸内陸</b> (北部山地) では両系統とも完全に皆無 = 海岸防御の対象外。</li>
  <li>件数: L64 + L65 統合で <b>{(len(l64_facil_df) if l64_facil_df is not None else 1645) + n_total_gates} 件</b> =
      広島県沿岸の防護網全体の物理的全貌。<b>静的 ({len(l64_facil_df) if l64_facil_df is not None else 1645}) + 動的 ({n_total_gates}) = 完全防御</b>。</li>
  <li><b>政策的含意</b>: 静的のみ / 動的のみの片方では沿岸防御は完結しない。
      静的は連続を担保し、動的は開口部を塞ぐ。<b>「動 + 静 = 完全」</b>の沿岸防御戦略を視覚的に確認。</li>
</ul>
"""

sec6 = (sec6_intro
        + "<h3>実装コード (L64 既扱との比較 + WKT 重ね合わせ)</h3>"
        + sec6_code
        + sec6_fig7_text
        + figure("assets/L65_fig7_l64_l65_compare.png",
                  f"図 7 (RQ3): L64 静的 vs L65 動的 件数比較 (全体 + 市町別)")
        + sec6_fig7_read
        + sec6_fig8_text
        + figure("assets/L65_fig8_l64_l65_overlay.png",
                  f"図 8 (RQ3): L64 静的 (緑線) + L65 動的 (赤点) 重ね合わせマップ — 県の海岸防御 動静 2 系統")
        + sec6_fig8_read
        + "<h3>表: L64 (静) vs L65 (動) 動静比較</h3>"
        + df_to_html(T_dynamic_static)
        + "<p><b>この表から読み取れること</b>: 動静の差は形状 (LineString vs POINT) と"
        " 操作 (受動 vs 能動) と 故障モード (劣化 vs 操作不良) で対照的。"
        " 同じ海岸法系の管理対象でも、運用思想が根本的に異なる。"
        " 動的施設は<b>「平時に操作能力を維持する」</b>ことが、静的施設は<b>「平時に物理状態を維持する」</b>ことが"
        " 防御能力の鍵。</p>"
        + "<h3>表: 市町別 L64 vs L65 件数比較 (Top 13)</h3>"
        + (df_to_html(T_city_compare_show) if not T_city_compare_show.empty else "<p>(L64 既扱データ無し)</p>")
        + (f"<p><b>この表から読み取れること</b>: 主要沿岸市町は L64 + L65 の両方が多く、"
        f"動静の重層防護を実現。<b>{city_compare.iloc[0]['市町名'] if not city_compare.empty else '尾道市'}</b> は"
        f"L65 動的が L64 静的の {city_compare.iloc[0]['L65_倍率'] if not city_compare.empty else 1.5:.2f} 倍 = 開口部の多さを反映。"
        f"L65 倍率が市町ごとに変動するのは、沿岸地形 (連続堤防の長さ vs 開口部数) のバランス差を反映。</p>"
        if not T_city_compare_show.empty else "")
        )

# Sec 7: 仮説検証総合
sec7 = (
    "<p>本記事の 5 仮説と観測結果の照合:</p>"
    + df_to_html(T_hypo)
    + "<h3>3 RQ × 3 結論</h3>"
    + "<ul>"
    + f"<li><b>RQ1 結論</b>: 広島県の防潮扉は<b>{n_total_gates} 件 / {n_cities} 市町 / {n_districts} 地区海岸 / {n_kinds} 種類 / {n_jurisdictions} 所管</b> の "
      f"多次元構造。<b>陸閘 {rikko_share:.0f}%</b> が圧倒的主役 (H1 強支持) で、"
      f"「堤防を横断する道路の開口部を閉じる」 という防潮扉の本旨を反映。"
      f"上位 3 市町 (<b>{city_count.iloc[0]['市町名']} / {city_count.iloc[1]['市町名']} / {city_count.iloc[2]['市町名']}</b>) で {top3_city_share:.0f}% (H2 {jud(top3_city_share>=40)}) = 偏在型分布。"
      f"地形類型 (多島海 + 軍港由来 + 都市沿岸) が動的施設整備を支配。</li>"
    + f"<li><b>RQ2 結論</b>: 防潮扉の<b>{l44_cover_pct:.0f}%</b>が高潮想定内、<b>{l49_cover_pct:.0f}%</b>が津波想定内。"
      f"差 = <b>{l44_cover_pct - l49_cover_pct:+.1f} pt</b> (H3 {jud(l44_cover_pct > l49_cover_pct)}) = "
      f"高潮カバーが津波カバーを上回る。"
      f"L64 (静的, +22.4 pt) と同方向の差を観測 = <b>動静両系統で「高潮主目的整備」 の歴史的痕跡が共通</b>。"
      f"<b>両方想定内 ({100*n_in_both/n_geom:.0f}%)</b> の多重ハザード地点は閉鎖時防御最重要、"
      f"<b>どちらも外 ({100*n_in_neither/n_geom:.0f}%)</b> は高所沿岸 + 内陸寄り扉。</li>"
    + f"<li><b>RQ3 結論</b>: L64 (静) vs L65 (動) 件数比 = <b>{n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f}</b> "
      f"(H4 {jud((n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645)) >= 1.5)}) = 動的施設が静的施設の 1.6 倍。"
      f"L64 港湾系シェア {l64_kowan_share:.0f}% vs L65 港湾系シェア {l65_kowan_share:.0f}% = "
      f"両方とも 7 割以上 (H5 {jud(l65_kowan_share>=70)}) で同じ管理者群が動静両系統を運用。"
      f"L64 + L65 統合で {(len(l64_facil_df) if l64_facil_df is not None else 1645) + n_total_gates} 件 = 広島県の海岸防御網全体。"
      f"<b>「動 + 静 = 完全」</b>の沿岸防御戦略を初めて空間データで定量化。</li>"
    + "</ul>"
    + "<h3>動的施設の本質と運用上の含意</h3>"
    + f"<p>本研究の最重要発見は<b>「動静密度比 = {n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f}」</b>と "
    f"<b>「港湾系所管シェア L64/L65 同程度」</b>の 2 点。"
    f"前者は<b>静的整備の 1.6 倍の操作員 + 訓練リソース</b>が必要であることを意味する。"
    f"後者は<b>同じ管理者群が動静両系統を一体運用している</b>制度設計を反映。"
    f"これらは防災行政の<b>「動的防御の運用コスト見積もり」</b>と "
    f"<b>「動静一体管理」</b>という新たな研究テーマを開く。</p>"
    + f"<p>動的施設の防御能力は<b>「物理的耐久性 × 操作の確実性」</b>の積で決まる。"
    f"L65 の<b>{n_in_l44} 件 ({l44_cover_pct:.0f}%) が高潮想定内</b>に位置する事実は、"
    f"これらの扉の<b>閉鎖が高潮被害を直接左右する</b>ことを意味する。"
    f"閉め忘れ・操作遅れ・開閉不良がそのまま被害に直結するため、"
    f"想定エリア内扉のリストは<b>避難計画 + 操作優先順位</b>の基礎資料となる。</p>"
    + "<h3>動静 2 系統の制度的位置付け</h3>"
    + f"<p>L64 (静的, 海岸法 1956) と L65 (動的, 海岸法 + 各施設所管法) は"
    f"<b>同じ法体系の異なる物理形態</b>。海岸法は施設の用途 (= 海岸保全) を規定するが、"
    f"具体的な施設形態 (= 静か動か) は地形と道路網の物理的必然で決まる。"
    f"連続堤防が必要な海岸では L64 が、堤防を貫通する道路 + 排水路がある海岸では L65 が必要になる。"
    f"両者は二者択一ではなく<b>相補的</b>に整備される。"
    f"本研究のデータはこの相補関係を 2,675 + 1,645 = 4,320 件の実データで初めて定量化した。</p>"
)

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

<h4>発展課題 1 (RQ1 由来): <b>整備年代</b>と扉老朽化の分析</h4>
<ul>
  <li><b>結果 X</b>: 本データには整備年代列が無いので、本研究では年代分析は未実施。
      しかし「過半は 2011 年以前」 と推定される (= 海岸法時代)。
      動的施設は<b>機械要素 (扉本体 + 可動部 + 駆動装置)</b> の老朽化が静的施設より速い。</li>
  <li><b>新仮説 Y</b>: 整備後 30 年以上経過した陸閘は<b>機械的故障率が顕著に上昇</b>し、
      「閉まらない」 「動かない」 リスクが高い。
      特に<b>1960-1980 年代の高度成長期</b>に集中整備された陸閘は、
      塩害 + 潮風腐食 + 駆動部摩耗で<b>耐用年数 30-40 年</b>を超えて運用されている可能性。</li>
  <li><b>課題 Z</b>: 広島県の<b>長寿命化計画</b> 公文書から整備年代をマッチング →
      老朽化リスクスコアを各扉に付与 → 高潮想定内 + 老朽 = <b>緊急対策候補リスト</b>を作成。
      <b>「老朽化 + ハザード + 動的故障率」 のクロスリスク地理学</b>として展開。</li>
</ul>

<h4>発展課題 2 (RQ1 拡張): <b>扉の規模 (高さ・幅) と操作機構</b>のスコア化</h4>
<ul>
  <li><b>結果 X</b>: 本研究では種類 (5 区分) で分類したが、<b>実際の扉サイズ・操作機構</b> (= 自動 / 手動)
      は未取得。同じ「陸閘」 でも 1m 級と 5m 級では性能 + 操作性が桁違い。</li>
  <li><b>新仮説 Y</b>: 扉サイズ + 自動/手動 区分を組み合わせて<b>「閉鎖速度スコア」</b>を算出可。
      自動扉は<b>遠隔操作 = 数分で閉鎖</b>、手動扉は<b>現地操作員 = 数十分</b>。
      警報発令から扉閉鎖までの時間差が被害規模を左右する。</li>
  <li><b>課題 Z</b>: 維持管理データ (=本データに含まれる「維持管理情報」 の詳細別ファイル想定) から
      扉サイズ + 操作機構を取得 → 閉鎖速度スコア算出 →
      高潮警報発令から閉鎖完了までの時系列シミュレーション。
      <b>「動的防御の時間応答性」</b>として展開。</li>
</ul>

<h4>発展課題 3 (RQ2 由来): <b>「想定エリア + 扉なし」 の沿岸線</b>の同定</h4>
<ul>
  <li><b>結果 X</b>: 本研究では「想定 + 扉」 の重なりを集計したが、<b>「想定 - 扉」 = 想定エリア内で扉の無い沿岸線</b>は
      未抽出。これは<b>動的防御の隙間</b>として最重要なエリア。</li>
  <li><b>新仮説 Y</b>: L44 高潮想定 polygon の<b>沿岸境界線</b>を抽出し、
      その境界線上の道路横断箇所で 100 m 以内に扉が無いセグメントは<b>「未動的防御開口部」</b>として
      重点整備候補になる。これは沿岸長 km 単位で定量化可能。</li>
  <li><b>課題 Z</b>: <code>l44_max.boundary</code> で polygon 境界線を取得 →
      OSM の道路データと交差判定 → 各交差点から最近傍扉までの距離を
      <code>shapely.STRtree</code> で計算 → <b>未防御交差点を地図化</b>。
      市町別に未防御交差点数を集計し、<b>「動的防御未整備度ランキング」</b>を作成。
      <b>「動的防御の地理的隙間学」</b>として展開。</li>
</ul>

<h4>発展課題 4 (RQ3 由来): <b>L64 + L65 の連携 — 同一沿岸線上での分担検証</b></h4>
<ul>
  <li><b>結果 X</b>: 本研究では L64 + L65 が主要港湾エリアで重複することを地理的に確認したが、
      <b>「L64 静的線の途中に L65 動的点が均等に並ぶ」</b>という予想を厳密に検証していない。</li>
  <li><b>新仮説 Y</b>: L64 line に対して L65 point が一定間隔 (= 100-300 m) で並ぶ分布が観測される。
      これは「堤防 100-300m に開口部 1 箇所」 の物理的密度を意味する。
      逆に L64 line から離れた孤立 L65 point は「堤防無しの単独陸閘」 = 道路直接遮断装置。</li>
  <li><b>課題 Z</b>: 各 L65 point から最近傍 L64 line までの距離を計算 →
      ヒストグラムで「近接 (< 50m) vs 孤立 (> 100m)」 を分類 →
      孤立 L65 の地理分布 + 機能解釈 (= 道路直接遮断装置) を可視化。
      <b>「L64 + L65 の物理的相補関係」</b>として展開。</li>
</ul>

<h4>発展課題 5 (RQ3 拡張): <b>L62 避難情報との連動</b></h4>
<ul>
  <li><b>結果 X</b>: 本データは<b>扉の物理的位置</b>のみで、<b>閉鎖タイミング</b> (= 何時間前に閉鎖開始) は未取得。
      避難情報 (L62) と扉閉鎖は連動するが、その時系列は別データ。</li>
  <li><b>新仮説 Y</b>: 高潮警報発令時刻 → 防潮扉閉鎖開始時刻 → 完全閉鎖時刻 → 避難勧告発令時刻 の
      <b>4 段階タイムライン</b>が地域ごとに固定されている。閉鎖完了が避難開始の前提条件になる場合あり。</li>
  <li><b>課題 Z</b>: 広島県の防災業務マニュアルから<b>扉閉鎖トリガー</b>を抽出 →
      L62 避難情報の発令タイムラインとマッチング → 動的防御 + 避難情報の時系列モデル化。
      <b>「動的防御 - 避難情報 連動運用学」</b>として展開。</li>
</ul>

<h4>発展課題 6 (RQ2 + L40 連携): <b>標高 + 扉位置</b>の統合分析</h4>
<ul>
  <li><b>結果 X</b>: 本研究では扉の地理位置のみ扱い、標高は未利用。
      高潮想定内扉の<b>標高</b>がどう分布するかは未確認。</li>
  <li><b>新仮説 Y</b>: 防潮扉は<b>低標高 (≤ 5m)</b> に集中するが、扉種類別に分布が異なる。
      陸閘 (= 道路上) は道路標高に従い、樋門 (= 排水路上) は排水路標高に従う。
      標高分布から「どの扉がより危険な低地に位置するか」 が分かる。</li>
  <li><b>課題 Z</b>: L40 の<b>5m DEM</b>から各扉 POINT の標高を抽出 →
      種類別 + 高潮率別ヒストグラム → <b>「標高分布の種類差 + 高潮重なり差」</b>を統計検定。
      <b>「標高 + 種類 + ハザード」 の 3 軸統合分析</b>として展開。</li>
</ul>

<h4>発展課題 7 (展望): <b>全国防潮扉データとの瀬戸内海特性</b>比較</h4>
<ul>
  <li><b>結果 X</b>: 本研究は広島県 1 県の {n_total_gates} 件を扱ったが、<b>全国比較</b>は未実施。
      他県 (静岡・高知・宮城等) の防潮扉と比較すれば、瀬戸内海の特性が浮き彫り。</li>
  <li><b>新仮説 Y</b>: 瀬戸内海諸県 (広島・岡山・香川・愛媛) の防潮扉は太平洋諸県 (静岡・高知) に比べ
      <b>(a) 件数が多く密配置</b>、<b>(b) 陸閘比率がさらに高い</b>、<b>(c) 沿岸 km あたりの扉密度が高い</b>。
      これは瀬戸内海の地形 (= 多島海 + 内海) と都市 + 港湾密集を反映する。</li>
  <li><b>課題 Z</b>: 国交省 + 各都道府県の防潮扉データを取得 → 種類別シェアで標準化 →
      県別 陸閘シェア・沿岸長あたり密度を比較。<b>「瀬戸内海動的防御の地形特殊性」</b>を統計化。
      L64 (海岸保全) の瀬戸内海特性と組み合わせ、<b>「瀬戸内海防災学」</b>の総合フレームへ展開。</li>
</ul>
"""

# 統合
sections = [
    ("学習目標と問い", sec1),
    ("使用データ", sec2),
    ("ダウンロード", sec3),
    (f"【RQ1】 防潮扉の構造 — 陸閘 {rikko_share:.0f}% / "
     f"{n_cities} 市町 / Top 3 で {top3_city_share:.0f}%",
     sec4),
    (f"【RQ2】 高潮 (L44) + 津波 (L49) カバレッジ — "
     f"高潮 {l44_cover_pct:.0f}% / 津波 {l49_cover_pct:.0f}%",
     sec5),
    (f"【RQ3】 L64 静的 vs L65 動的 役割分担 — "
     f"L65/L64 = {n_total_gates / (len(l64_facil_df) if l64_facil_df is not None else 1645):.2f} 倍",
     sec6),
    ("仮説検証総合", sec7),
    ("発展課題", sec8),
]


html = render_lesson(
    num=65,
    title="防潮扉（水門・陸閘）単独 3 研究例分析 — "
          f"{n_total_gates} 件から県の動的防御戦略を読む",
    tags=["L65", "防潮扉", "陸閘", "水門", "樋門",
          "RQ×3", "Format B", "geopandas", "POINT (CSV)",
          "choropleth", "L64連携 (静的施設)", "L44連携 (高潮)",
          "L49連携 (津波)", "動静分担", "最後のバリア"],
    time="50 分",
    level="中級",
    data_label=f"DoBoX dataset 1249 (CSV, ~497 KB) "
               f"+ L64 既扱 + L44/L49 既キャッシュ",
    sections=sections,
    script_filename="L65_floodgates.py",
)

OUT_HTML = LESSONS / "L65_floodgates.html"
OUT_HTML.write_text(html, encoding="utf-8")

print(f"  HTML: {OUT_HTML.name} ({len(html):,} chars)")
print(f"  ({time.time()-t10:.1f}s)", flush=True)


# =============================================================================
# 終了
# =============================================================================
print(f"\n=== 完了 (合計 {time.time()-t_all:.1f} 秒) ===", flush=True)
