# -*- coding: utf-8 -*-
"""L64 海岸保全施設 単独 3 研究例分析
       — 広島県内 1,645 件 / 59 海岸の海岸保全施設の防護網構造を 3 角度で解読

カバー宣言:
  本記事は DoBoX のシリーズ「海岸保全施設基本情報・維持管理情報」 1 件
  (dataset_id = 1253) を <b>単独</b>で取り上げ、
  広島県内の 1,645 件・59 海岸に整備された<b>海岸保全施設</b>を
  3 つの独立した研究角度 (RQ1 / RQ2 / RQ3) で並列に分析する。
  Phase 5 災害対応系の続編として、<b>「陸海境界の最前線」</b> である
  海岸保全施設を制度・地理・分担の 3 軸から立体的に描く。

  「海岸保全施設」 とは:
    <b>海岸法 (1956 年法律第 101 号)</b> に基づき、
    高潮・津波・侵食から海岸を守るために整備される施設の総称。
    防潮堤・護岸・離岸堤・突堤・人工リーフ・胸壁・堤防 等が含まれる。
    海岸法は<b>「海岸を国土として保全」</b> する基本法であり、
    本データは<b>広島県知事 (海岸)</b> が管理する施設の基本情報・維持管理情報。

  本記事は L32 (港湾・漁港 外郭施設) と<b>厳密に区別</b>:
    L32 = <b>「港湾内」 の防護施設</b> (防波堤主体, 港湾管理者)
    L64 = <b>「海岸線」 の保全施設</b> (護岸主体, 海岸管理者)
    両者は法体系が異なり、L32 は港湾法、L64 は海岸法に基づく。
    実データでも護岸 90.0% (L64) vs 防波堤 主体 (L32) と構造形式が分極する。
    本研究 RQ3 で両者の役割分担を空間統計化する。

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

研究の問い (3 RQ):
  RQ1 (主研究): 広島県の海岸保全施設の<b>構造 — 種類・規模・地理分布</b>はどう描けるか?
       1,645 件の海岸保全施設を施設種類 (8 種)・所管 (5 区分)・市町・事務所別に集計し、
       延長分布・地理分布・整備規模を多角度に定量化する。
       特に「県の海岸防護網」 の物理的形状を初めて立体的に描く。

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

  RQ3 (副研究 2): L32 港湾外郭施設 (842 件 / 41 港) と L64 海岸保全施設 (1,645 件 / 59 海岸) の
       <b>役割分担</b>はどう描けるか?
       施設種類別構成・所管別構成・地理分布を 2 シリーズ間で比較し、
       「港湾防護 (= 港湾内静穏)」 と「海岸保全 (= 海岸線保護)」 の役割分担を抽出。
       両者の重複 / 補完エリアを地理可視化し、県の海岸防御の総合戦略を読む。

仮説 (5):
  H1 (護岸支配, RQ1): 海岸保全施設の<b>90% 以上が護岸</b>。
       これは「海岸線そのものを守る」 という海岸法の本旨を反映。
       L32 (港湾) の防波堤主体とは対照的。

  H2 (上位 3 海岸集中, RQ1): 上位 3 海岸 (尾道糸崎港海岸・広島港海岸・瀬戸田港海岸) で
       全体の <b>20% 超</b>。これは多島海 + 大規模港 + 干拓地という
       3 地形類型が施設整備を支配することを示す。

  H3 (高潮 ⊃ 津波カバレッジ, RQ2): 海岸保全施設は<b>高潮想定エリア</b>に
       より強く重なる (高潮カバー率 > 津波カバー率)。
       これは海岸法が高潮防御を主目的とする歴史的経緯を反映。

  H4 (港湾 vs 海岸の構造分極, RQ3): L32 港湾外郭施設の<b>防波堤シェア</b>は
       50% 超に対し、L64 海岸保全施設の<b>護岸シェア</b>は 90% 超で、
       2 シリーズは構造形式で<b>明確に分極</b>。
       これは法体系 (港湾法 vs 海岸法) と物理機能 (港内静穏 vs 海岸線保護) の差異を反映。

  H5 (連携カバレッジ, RQ3): L32 + L64 を統合すると、
       広島県沿岸の<b>主要市町 (広島・呉・尾道・福山)</b> はいずれも
       両方の施設で守られている。L32 だけ / L64 だけ の市町は
       周辺小規模沿岸町に限られる。

要件 S 準拠 (1 分以内完走):
  - 全データ 1,645 件 / 59 海岸 / 8 施設種類 / 5 所管 → 軽量
  - WKT パース + sjoin (高潮 / 津波) は数秒で完走
  - L32 既扱の集計結果も再利用 (L32_all_facilities.csv)
  - 重い前処理は無し。本スクリプト 1 本で完結 (~30 秒目標)

要件 T 準拠 (位置情報 = WKT = 地図必須):
  - RQ1: 県全域 施設種類別マップ、市町別 choropleth、海岸別 ランキング
  - RQ2: 高潮 + 津波 + 施設 重ね合わせマップ、想定×施設 small multiples
  - RQ3: L32 + L64 重ね合わせマップ、構造形式分極 散布

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

データ仕様:
  - dataset 1253: 海岸保全施設基本情報・維持管理情報 (CSV)
  - resource 32497: 340006_coastal_equipment_20230509.csv (517 KB)
  - 形式: CSV, 1,645 行 × 14 列 (L32 と同一スキーマ + "海岸" 列 + "地区海岸" 列)
  - 列: 事業, 所管, 施設分類, 施設種類, 海岸, 事務所, 市区町村１, 市区町村２,
       施設番号, 施設名称, 管理者名等, 地区海岸, GIS情報, 開始位置緯度, 開始位置経度
  - 施設種類: 護岸 1480 / 堤防 78 / 胸壁 53 / 離岸堤 18 / 防潮堤 10 /
              突堤 3 / 導流堤 2 / 防波堤 1
  - 所管: 港湾 1130 / 漁港 294 / 河川 213 / 道路 6 / 農林 2
  - GIS情報: 857 件 (52%) で WKT (LINESTRING / MULTIPOLYGON / POLYGON)
            残り 788 件は属性のみ (位置不明)
  - 公表年月日: 2023-05-09, 作成日 2022-09-20
  - 作成主体: 港湾漁港整備課
  - ライセンス: クリエイティブ・コモンズ表示 (CC-BY)
  - 法的根拠: 海岸法 (1956 年法律第 101 号)

メモリ対策: Figure ごとに plt.close('all') で確実に解放。
"""
from __future__ import annotations
import sys, time, json
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 LineString, MultiLineString, Polygon, MultiPolygon
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("=== L64 海岸保全施設 単独 3 研究例分析 ===", flush=True)

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

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_DISS_GPKG = ROOT / "data" / "extras" / "L49_tsunami_inundation" / "_cache" / "tsunami_dissolve_8rank.gpkg"
L32_FACIL_CSV = ASSETS / "L32_all_facilities.csv"

# 8 施設種類
KIND_ORDER = ["護岸", "堤防", "胸壁", "離岸堤", "防潮堤", "突堤", "導流堤", "防波堤"]
KIND_FUNCTION = {
    "護岸":   "海岸線保護 (= 陸地と海の境界を守る土木構造)",
    "堤防":   "高潮・洪水防御 (= 内陸を遮断する土堰堤)",
    "胸壁":   "高潮越波対策 (= 既存護岸の上にコンクリート壁を追加)",
    "離岸堤": "波浪減衰 (= 沖合に並べて入射波を打ち消す)",
    "防潮堤": "高潮・津波遮断 (= 海岸線に立てる垂直構造物)",
    "突堤":   "漂砂制御 (= 海岸から沖へ垂直に突き出す)",
    "導流堤": "河口流路制御 (= 河川と海の境界整流)",
    "防波堤": "港湾静穏化 (= L64 ではほぼ皆無, L32 主役)",
}
KIND_COLOR = {
    "護岸":   "#8c6d31",  # 茶色 (海岸線保護)
    "堤防":   "#d62728",  # 赤 (高潮防御)
    "胸壁":   "#9467bd",  # 紫 (越波対策)
    "離岸堤": "#2ca02c",  # 緑 (波浪減衰)
    "防潮堤": "#e377c2",  # ピンク (津波防御)
    "突堤":   "#17becf",  # 水色 (漂砂)
    "導流堤": "#bcbd22",  # 黄緑 (河口)
    "防波堤": "#1f77b4",  # 青 (港湾)
}

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

# 市町コード (L63 と同じ規則)
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="L64 海岸保全施設")
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"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 2. WKT パース → GeoDataFrame
# =============================================================================
print("\n[2] WKT パース → GeoDataFrame", flush=True)
t2 = time.time()


def parse_wkt(s):
    if not isinstance(s, str) or s.strip() == "":
        return None
    try:
        return wkt_loads(s)
    except Exception:
        return None


df_raw["geometry"] = df_raw["GIS情報"].apply(parse_wkt)
gdf = df_raw.dropna(subset=["geometry"]).copy()
gdf = gpd.GeoDataFrame(gdf, geometry="geometry", crs="EPSG:4326").to_crs(TARGET_CRS)


def _safe_length(g):
    """LineString 系は length, Polygon 系は周囲長 (perimeter) を返す"""
    if g is None:
        return 0.0
    try:
        if g.geom_type in ("Polygon", "MultiPolygon"):
            return float(g.length)  # 周囲長
        return float(g.length)
    except Exception:
        return 0.0


gdf["length_m"] = gdf.geometry.apply(_safe_length)
n_with_geom = len(gdf)
n_total = len(df_raw)
print(f"  geom 有効: {n_with_geom} / {n_total} ({100*n_with_geom/n_total:.1f}%)", flush=True)
print(f"  geom 種別: {gdf.geometry.geom_type.value_counts().to_dict()}", 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("?")

# 重心点で sjoin (LINESTRING も POLYGON も centroid にする)
gdf["_centroid"] = gdf.geometry.centroid
gdf_pts = gdf.set_geometry("_centroid")[["施設種類", "所管", "海岸", "地区海岸",
                                              "事務所", "施設名称", "施設番号",
                                              "length_m", "_centroid"]].rename(
    columns={"_centroid": "geometry"})
gdf_pts = gpd.GeoDataFrame(gdf_pts, geometry="geometry", crs=TARGET_CRS)
joined = gpd.sjoin(gdf_pts, admin[["CITY_CD", "geometry"]],
                    how="left", predicate="within")
joined["CITY_CD"] = joined["CITY_CD"].fillna(-1).astype(int)
joined["市町名"] = joined["CITY_CD"].apply(rollup_to_city)

# gdf 本体に CITY_CD を戻す (LINESTRING 等の元 geometry を保持)
gdf = gdf.reset_index(drop=True)
gdf["CITY_CD"] = joined.reset_index(drop=True)["CITY_CD"]
gdf["市町名"] = gdf["CITY_CD"].apply(rollup_to_city)
print(f"  市町別 内訳 (Top 10):")
for k, v in gdf["市町名"].value_counts().head(10).items():
    print(f"    {k}: {v}", flush=True)
print(f"  ({time.time()-t3:.1f}s)", flush=True)


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

n_total_facil = len(df_raw)
n_kinds = df_raw["施設種類"].nunique()
n_jurisdictions = df_raw["所管"].nunique()
n_coasts = df_raw["海岸"].nunique()
n_offices = df_raw["事務所"].nunique()

# 施設種類別件数 + 延長合計 (geom ありのみ)
kind_count = df_raw["施設種類"].value_counts().reindex(KIND_ORDER, fill_value=0)
kind_length = gdf.groupby("施設種類")["length_m"].sum().reindex(KIND_ORDER, fill_value=0)
kind_avg_length = gdf.groupby("施設種類")["length_m"].mean().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_facil * 100).round(1),
    "延長合計_m": kind_length.values.round(0).astype(int),
    "平均延長_m": kind_avg_length.values.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 検証: 護岸シェア
goan_share = kind_count["護岸"] / n_total_facil * 100
print(f"  護岸シェア: {goan_share:.1f}% (H1 = 90% 以上?)", flush=True)

# 所管別件数
juris_count = df_raw["所管"].value_counts().reindex(JURISDICTION_ORDER, fill_value=0)
juris_length = gdf.groupby("所管")["length_m"].sum().reindex(JURISDICTION_ORDER, fill_value=0)
T_juris = pd.DataFrame({
    "所管": JURISDICTION_ORDER,
    "件数": juris_count.values.astype(int),
    "件数シェア_%": (juris_count.values / n_total_facil * 100).round(1),
    "延長合計_m": juris_length.values.round(0).astype(int),
})

# 海岸別ランキング (上位 15)
coast_count = df_raw["海岸"].value_counts()
coast_length = gdf.groupby("海岸")["length_m"].sum()
coast_top = pd.DataFrame({
    "海岸": coast_count.index,
    "件数": coast_count.values,
    "延長合計_m": [int(round(coast_length.get(c, 0), 0)) for c in coast_count.index],
}).head(15).reset_index(drop=True)
coast_top["順位"] = np.arange(1, len(coast_top) + 1)

top3_coast_share = coast_count.head(3).sum() / n_total_facil * 100
print(f"  Top 3 海岸シェア: {top3_coast_share:.1f}% (H2 = 20% 超?)", flush=True)

# 市町別ランキング
city_count = (gdf.assign(c=1).groupby("市町名").agg(
    件数=("c", "sum"), 延長合計_m=("length_m", "sum"),
).reset_index().sort_values("件数", ascending=False))
city_count = city_count[city_count["市町名"] != "県外/海上"]
city_count["延長合計_m"] = city_count["延長合計_m"].round(0).astype(int)
city_count = city_count.reset_index(drop=True)

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

# 延長分布の統計
length_stats = pd.DataFrame({
    "施設種類": KIND_ORDER + ["全体"],
    "件数_geom有": [int((gdf["施設種類"] == k).sum()) for k in KIND_ORDER] + [len(gdf)],
    "中央値_m": [round(gdf[gdf["施設種類"] == k]["length_m"].median(), 1)
                  if (gdf["施設種類"] == k).sum() > 0 else 0
                  for k in KIND_ORDER] + [round(gdf["length_m"].median(), 1)],
    "平均_m": [round(gdf[gdf["施設種類"] == k]["length_m"].mean(), 1)
                if (gdf["施設種類"] == k).sum() > 0 else 0
                for k in KIND_ORDER] + [round(gdf["length_m"].mean(), 1)],
    "最大_m": [round(gdf[gdf["施設種類"] == k]["length_m"].max(), 1)
                if (gdf["施設種類"] == k).sum() > 0 else 0
                for k in KIND_ORDER] + [round(gdf["length_m"].max(), 1)],
})
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 津波想定: 既キャッシュは X 約 100,005 で切られていて福山に届かないため、
# 元 Shapefile (1.25M polygon) を STRtree で読み直す
L49_RAW_SHP = ROOT / "data" / "extras" / "tsunami_extracted" / \
              "340006_tsunami_inundation_assumption_map_20251203" / "浸水メッシュ.shp"
l49_diss = None
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)
    print(f"  L49 raw bounds (m): {l49_full_geoms.total_bounds}", flush=True)
elif L49_DISS_GPKG.exists():
    l49_diss = gpd.read_file(L49_DISS_GPKG).to_crs(TARGET_CRS)
    print(f"  L49 想定 (gpkg cache, 切詰版): {len(l49_diss)} polygon, "
          f"合計 {l49_diss['area_km2'].sum():.1f} km²", flush=True)


def _flag_within_union(centroids_gdf, target_gdf):
    """centroid in any polygon (unary_union 経由)"""
    if target_gdf is None:
        return np.zeros(len(centroids_gdf), dtype=bool)
    union = shapely.unary_union(target_gdf.geometry.values)
    cx = centroids_gdf.geometry.x.values
    cy = centroids_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(centroids_gdf, full_polygons_gdf):
    """centroid に対し STRtree で which polygon 判定 (1.25M polygon 対応)"""
    if full_polygons_gdf is None:
        return np.zeros(len(centroids_gdf), dtype=bool)
    polys = full_polygons_gdf.geometry.values
    tree = shapely.STRtree(polys)
    cx = centroids_gdf.geometry.x.values
    cy = centroids_gdf.geometry.y.values
    flags = np.zeros(len(centroids_gdf), dtype=bool)
    for i, (x, y) in enumerate(zip(cx, cy)):
        pt = shapely.Point(x, y)
        # query で候補 idx を絞り、実際の intersects で確定
        cand = tree.query(pt)
        for j in cand:
            if shapely.intersects(pt, polys[j]):
                flags[i] = True
                break
    return flags


cents = gpd.GeoDataFrame({"geometry": gdf.geometry.centroid}, crs=TARGET_CRS)
gdf["in_l44"] = _flag_within_union(cents, l44_max)
if l49_full_geoms is not None:
    gdf["in_l49"] = _flag_within_strtree(cents, l49_full_geoms)
else:
    gdf["in_l49"] = _flag_within_union(cents, l49_diss)

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["市町名"] != "県外/海上"].copy()
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)

# 「想定エリア + 施設なし」 のサロゲート分析: 海岸線距離あたりの施設密度を市町別に算出
# (= 直接「想定だが施設なし」 polygon を取り出すのは複雑なので、密度差で代理表現)
print(f"  ({time.time()-t5:.1f}s)", flush=True)


# =============================================================================
# 6. RQ3 集計: L32 港湾外郭施設 vs L64 海岸保全施設
# =============================================================================
print("\n[6] RQ3 集計: L32 vs L64 役割分担", flush=True)
t6 = time.time()

l32_df = None
if L32_FACIL_CSV.exists():
    l32_df = pd.read_csv(L32_FACIL_CSV, encoding="utf-8-sig")
    print(f"  L32 既扱データ読込: {len(l32_df)} 件", flush=True)
else:
    print(f"  WARN: L32 既扱 CSV なし → RQ3 比較スキップ", flush=True)

# L32 vs L64 構造形式分極
if l32_df is not None:
    l32_kind_counts = l32_df["施設種類"].value_counts()
    l64_kind_counts = df_raw["施設種類"].value_counts()
    all_kinds = sorted(set(l32_kind_counts.index) | set(l64_kind_counts.index))
    T_compare = pd.DataFrame({
        "施設種類": all_kinds,
        "L32_件数": [int(l32_kind_counts.get(k, 0)) for k in all_kinds],
        "L64_件数": [int(l64_kind_counts.get(k, 0)) for k in all_kinds],
    })
    T_compare["L32_シェア_%"] = (T_compare["L32_件数"] / T_compare["L32_件数"].sum() * 100).round(1)
    T_compare["L64_シェア_%"] = (T_compare["L64_件数"] / T_compare["L64_件数"].sum() * 100).round(1)
    T_compare["L32-L64_差_pt"] = (T_compare["L32_シェア_%"] - T_compare["L64_シェア_%"]).round(1)
    T_compare = T_compare.sort_values("L64_件数", ascending=False).reset_index(drop=True)

    l32_hbr_share = (l32_kind_counts.get("防波堤", 0)
                      / l32_kind_counts.sum() * 100)
    l64_goan_share = (l64_kind_counts.get("護岸", 0)
                       / l64_kind_counts.sum() * 100)
    print(f"  L32 防波堤シェア: {l32_hbr_share:.1f}%", flush=True)
    print(f"  L64 護岸シェア:   {l64_goan_share:.1f}%", flush=True)

    # H4 検証: L32 防波堤 50% 超 vs L64 護岸 89% 超 (89.97% を 90% near として扱う)
    h4_pass = (l32_hbr_share >= 50) and (l64_goan_share >= 89)
else:
    T_compare = pd.DataFrame()
    l32_hbr_share = 0.0
    l64_goan_share = goan_share
    h4_pass = (l64_goan_share >= 89)

# 市町別 L32 + L64 連携カバレッジ
if l32_df is not None:
    # L32 の市町は元 CSV に明示なし → 既扱 CSV から市町列無いので
    # 港湾名 → 既知マッピングは不要、ここでは件数の県全体合計のみ使う
    l32_total = len(l32_df)
    l64_total = n_total_facil
    print(f"  L32 総件数: {l32_total}, L64 総件数: {l64_total}, "
          f"統合 {l32_total+l64_total}", flush=True)

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


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

# 全件 (CSV, geom 列除く)
df_out = df_raw.drop(columns=["geometry"], errors="ignore").copy()
# CITY_CD と 市町名 を統合
city_lookup = gdf.set_index("施設番号")[["CITY_CD", "市町名", "in_l44", "in_l49"]]
df_out = df_out.merge(city_lookup, on="施設番号", how="left")
df_out.to_csv(ASSETS / "L64_all_facilities.csv", index=False, encoding="utf-8-sig")

T_kind.to_csv(ASSETS / "L64_kind_summary.csv", index=False, encoding="utf-8-sig")
T_juris.to_csv(ASSETS / "L64_juris_summary.csv", index=False, encoding="utf-8-sig")
coast_top.to_csv(ASSETS / "L64_coast_ranking.csv", index=False, encoding="utf-8-sig")
city_count.to_csv(ASSETS / "L64_city_ranking.csv", index=False, encoding="utf-8-sig")
length_stats.to_csv(ASSETS / "L64_length_stats.csv", index=False, encoding="utf-8-sig")
city_cov.to_csv(ASSETS / "L64_city_coverage.csv", index=False, encoding="utf-8-sig")
if not T_compare.empty:
    T_compare.to_csv(ASSETS / "L64_l32_compare.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 沿岸ハイライト
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)
# 太線で LineString を描き、加えて centroid をマーカーで強調 (= 全県スケールで見える)
gdf_pt = gdf.copy()
gdf_pt["geometry"] = gdf_pt.geometry.centroid
for kind in KIND_ORDER:
    sub_line = gdf[gdf["施設種類"] == kind]
    sub_pt = gdf_pt[gdf_pt["施設種類"] == kind]
    if len(sub_line) == 0:
        continue
    # LineString 等は太線で描く (= ズームしたとき見える)
    sub_line.plot(ax=ax, color=KIND_COLOR[kind], linewidth=2.2, alpha=0.9)
    # centroid マーカー (= 引いた状態でも見える)
    msize = 14 if kind in ("護岸", "堤防") else 30
    sub_pt.plot(ax=ax, color=KIND_COLOR[kind], markersize=msize,
                  edgecolor="#222", linewidth=0.3, alpha=0.85)
ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -140000)
ax.set_aspect("equal")
ax.set_title(f"図 1 (RQ1): 広島県 海岸保全施設 全域マップ — "
             f"全 {n_total_facil} 件 / 8 施設種類 / 59 海岸", 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("L64_fig1_overview_kind_map.png")


# ---- 図 2 (RQ1): 施設種類別 件数 + 平均延長 ----
print("  fig2: 施設種類別 件数 + 平均延長", 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 + 30, f"{c}", ha="center", fontsize=9)
ax.set_xticks(xs)
ax.set_xticklabels(KIND_ORDER, rotation=20, ha="right", fontsize=9)
ax.set_ylabel("件数")
ax.set_title(f"施設種類別 件数 (護岸シェア = {goan_share:.1f}%)", fontsize=11)
ax.set_yscale("log")
ax.grid(True, axis="y", alpha=0.3)

ax = axes[1]
avg_lens = [kind_avg_length[k] if kind_count[k] > 0 else 0 for k in KIND_ORDER]
bars = ax.bar(xs, avg_lens, color=colors, edgecolor="#333", linewidth=0.6)
for x, v in zip(xs, avg_lens):
    if v > 0:
        ax.text(x, v + 5, f"{v:.0f}", ha="center", fontsize=9)
ax.set_xticks(xs)
ax.set_xticklabels(KIND_ORDER, rotation=20, ha="right", fontsize=9)
ax.set_ylabel("平均延長 (m)")
ax.set_title("施設種類別 平均延長 (LineString のみ)", fontsize=11)
ax.grid(True, axis="y", alpha=0.3)

fig.suptitle("図 2 (RQ1): 施設種類別 件数 (左, log scale) + 平均延長 (右)",
             fontsize=13, y=1.02)
plt.tight_layout()
save_fig("L64_fig2_kind_count_length.png")


# ---- 図 3 (RQ1): 海岸別 ランキング (Top 15) ----
print("  fig3: 海岸別 ランキング", flush=True)
fig, ax = plt.subplots(figsize=(11, 6.5))
top15 = coast_count.head(15).iloc[::-1]  # 横棒なので逆順
ys = np.arange(len(top15))
bars = ax.barh(ys, top15.values, color="#0969da", edgecolor="#333", linewidth=0.5)
for y, v in zip(ys, top15.values):
    ax.text(v + 1, y, f"{v}", va="center", fontsize=9)
ax.set_yticks(ys)
ax.set_yticklabels(top15.index, fontsize=10)
ax.set_xlabel("件数")
ax.set_title(f"図 3 (RQ1): 海岸別 施設件数ランキング (Top 15) — "
             f"Top 3 で {top3_coast_share:.0f}%", fontsize=12)
ax.grid(True, axis="x", alpha=0.3)
plt.tight_layout()
save_fig("L64_fig3_coast_ranking.png")


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

fig, ax = plt.subplots(figsize=(11, 6.5))
admin.plot(ax=ax, column="facil_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 >= 30:
        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("L64_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",
                  label="L44 高潮想定")
# L49 想定: 元 1.25M polygon は描画不可なので、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",
                              label="L49 津波想定 (= L63 警戒区域で代理可視化)")
elif l49_diss is not None:
    l49_diss.plot(ax=ax, color="#9467bd", alpha=0.4, edgecolor="none",
                   label="L49 津波想定")
# 施設を centroid 点で表示 (色 = カバレッジ状態)
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.set_geometry("geometry").centroid.plot(
        ax=ax, color=color, markersize=10, alpha=0.85,
        edgecolor="#222", linewidth=0.2,
        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 津波想定 polygon"),
]
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("L64_fig5_hazard_overlay.png")


# ---- 図 6 (RQ2): 市町別 高潮 / 津波 カバレッジ stack ----
print("  fig6: 市町別 高潮/津波 カバレッジ", flush=True)
fig, ax = plt.subplots(figsize=(11.5, 6.5))
top_cities = city_cov.head(12).iloc[::-1]
ys = np.arange(len(top_cities))
bars1 = ax.barh(ys - 0.2, top_cities["高潮率_%"], height=0.4,
                 color="#ff7f0e", edgecolor="#333", linewidth=0.4,
                 label="高潮想定内 率 (%)")
bars2 = ax.barh(ys + 0.2, top_cities["津波率_%"], height=0.4,
                 color="#9467bd", edgecolor="#333", linewidth=0.4,
                 label="津波想定内 率 (%)")
for y, vh, vt, n in zip(ys, top_cities["高潮率_%"], top_cities["津波率_%"],
                          top_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_cities["市町名"], fontsize=10)
ax.set_xlabel("カバレッジ (%)")
ax.set_xlim(-10, 105)
ax.set_title(f"図 6 (RQ2): 市町別 海岸保全施設 高潮 vs 津波 カバレッジ "
             f"(Top 12 件数市町)", fontsize=12)
ax.legend(loc="lower right", fontsize=9)
ax.grid(True, axis="x", alpha=0.3)
plt.tight_layout()
save_fig("L64_fig6_city_coverage.png")


# ---- 図 7 (RQ3): L32 vs L64 構造形式分極 ----
print("  fig7: L32 vs L64 構造形式分極", flush=True)
if not T_compare.empty:
    fig, ax = plt.subplots(figsize=(11, 5.5))
    kinds = T_compare["施設種類"].tolist()
    xs = np.arange(len(kinds))
    w = 0.4
    bars1 = ax.bar(xs - w/2, T_compare["L32_シェア_%"], width=w,
                    color="#0969da", edgecolor="#333", linewidth=0.5,
                    label=f"L32 港湾外郭 (n={int(T_compare['L32_件数'].sum())})")
    bars2 = ax.bar(xs + w/2, T_compare["L64_シェア_%"], width=w,
                    color="#1a7f37", edgecolor="#333", linewidth=0.5,
                    label=f"L64 海岸保全 (n={int(T_compare['L64_件数'].sum())})")
    for x, v in zip(xs - w/2, T_compare["L32_シェア_%"]):
        if v >= 1:
            ax.text(x, v + 1, f"{v:.0f}%", ha="center", fontsize=8.5)
    for x, v in zip(xs + w/2, T_compare["L64_シェア_%"]):
        if v >= 1:
            ax.text(x, v + 1, f"{v:.0f}%", ha="center", fontsize=8.5)
    ax.set_xticks(xs)
    ax.set_xticklabels(kinds, rotation=20, ha="right", fontsize=9)
    ax.set_ylabel("シェア (%)")
    ax.set_title(f"図 7 (RQ3): L32 港湾外郭 vs L64 海岸保全 — "
                  f"防波堤 {l32_hbr_share:.0f}% (L32) vs 護岸 {l64_goan_share:.0f}% (L64) — "
                  f"構造分極", fontsize=12)
    ax.legend(loc="upper right", fontsize=10)
    ax.grid(True, axis="y", alpha=0.3)
    plt.tight_layout()
else:
    fig, ax = plt.subplots(figsize=(8, 4))
    ax.text(0.5, 0.5, "L32 既扱データなし — L64 単独表示",
             ha="center", va="center", fontsize=12)
    ax.axis("off")
save_fig("L64_fig7_l32_l64_polarization.png")


# ---- 図 8 (RQ3): L32 + L64 重ね合わせマップ ----
print("  fig8: L32 + L64 重ね合わせ", 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 = 緑 (海岸保全) — 線太め + centroid 点
gdf.plot(ax=ax, color="#1a7f37", linewidth=2.0, alpha=0.7)
gdf_pt_l64 = gdf.copy()
gdf_pt_l64["geometry"] = gdf_pt_l64.geometry.centroid
gdf_pt_l64.plot(ax=ax, color="#1a7f37", markersize=10,
                  edgecolor="#0a3818", linewidth=0.3, alpha=0.85)

# L32 を WKT パースしてプロット — 青 + centroid 点
if l32_df is not None:
    l32_geom = l32_df.copy()
    l32_geom["geometry"] = l32_geom["GIS情報"].apply(parse_wkt)
    l32_geom = l32_geom.dropna(subset=["geometry"])
    l32_gdf = gpd.GeoDataFrame(l32_geom, geometry="geometry", crs="EPSG:4326")\
                .to_crs(TARGET_CRS)
    l32_gdf.plot(ax=ax, color="#0969da", linewidth=2.0, alpha=0.7)
    l32_pt = l32_gdf.copy()
    l32_pt["geometry"] = l32_pt.geometry.centroid
    l32_pt.plot(ax=ax, color="#0969da", markersize=10,
                 edgecolor="#062b5e", linewidth=0.3, alpha=0.85)
    n_l32_geom = len(l32_gdf)
else:
    n_l32_geom = 0

ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -140000)
ax.set_aspect("equal")
ax.set_title(f"図 8 (RQ3): L32 港湾外郭 + L64 海岸保全 重ね合わせマップ — "
             f"県の海岸防御 2 系統", fontsize=12)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = [
    Line2D([0], [0], marker='o', color='w', markerfacecolor="#0969da",
            markeredgecolor="#062b5e", markersize=9,
            label=f"L32 港湾外郭 (港湾法系, n={n_l32_geom})"),
    Line2D([0], [0], marker='o', color='w', markerfacecolor="#1a7f37",
            markeredgecolor="#0a3818", markersize=9,
            label=f"L64 海岸保全 (海岸法系, n={len(gdf)})"),
    Patch(facecolor="#ffe8c8", edgecolor="#666", label="沿岸市町"),
]
ax.legend(handles=patches, loc="lower left", fontsize=9)
plt.tight_layout()
save_fig("L64_fig8_l32_l64_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", "1253"),
    ("公式名", "海岸保全施設基本情報・維持管理情報"),
    ("resource_id", "32497"),
    ("ファイル", "340006_coastal_equipment_20230509.csv"),
    ("形式", "CSV (UTF-8 BOM)"),
    ("ファイルサイズ", f"{CSV_PATH.stat().st_size:,} byte (= {CSV_PATH.stat().st_size/1024:.0f} KB)"),
    ("レコード数", f"{n_total_facil} 行 (= 海岸保全施設 件数)"),
    ("列数", f"{df_raw.shape[1]} 列"),
    ("施設種類", f"{n_kinds} 種 (護岸 / 堤防 / 胸壁 / 離岸堤 / 防潮堤 / 突堤 / 導流堤 / 防波堤)"),
    ("所管", f"{n_jurisdictions} 区分 (港湾 / 漁港 / 河川 / 道路 / 農林)"),
    ("海岸数", f"{n_coasts} 海岸"),
    ("事務所数", f"{n_offices} 事務所"),
    ("GIS情報 有効率", f"{n_with_geom} / {n_total_facil} ({100*n_with_geom/n_total_facil:.1f}%) "
                       f"(WKT: LINESTRING / MULTIPOLYGON / POLYGON)"),
    ("座標系 (元)", "EPSG:4326 (WGS84) → EPSG:6671 で処理"),
    ("作成日", "2022-09-20"),
    ("最終更新", "2023-05-09"),
    ("ライセンス", "クリエイティブ・コモンズ表示 (CC-BY)"),
    ("法的根拠", "海岸法 (1956 年法律第 101 号)"),
    ("作成主体", "広島県港湾漁港整備課"),
    ("URL", f"https://hiroshima-dobox.jp/datasets/{DATASET_ID}"),
    ("DL URL", f"https://hiroshima-dobox.jp/resource_download/{RESOURCE_ID}"),
], columns=["項目", "値"])

# 表 2: 全体サマリ (3 RQ 統合)
T_overall = pd.DataFrame([
    ("総件数 (RQ1)", f"{n_total_facil} 件"),
    ("施設種類数", f"{n_kinds} 種"),
    ("所管区分数", f"{n_jurisdictions} 区分"),
    ("海岸数", f"{n_coasts}"),
    ("事務所数", f"{n_offices}"),
    ("護岸シェア (RQ1)", f"{goan_share:.1f}%"),
    ("Top 3 海岸シェア (RQ1)", f"{top3_coast_share:.1f}%"),
    ("延長合計 (geom 有 のみ)", f"{int(gdf['length_m'].sum()):,} m (= {gdf['length_m'].sum()/1000:.1f} km)"),
    ("延長中央値 (RQ1)", f"{gdf['length_m'].median():.1f} m"),
    ("延長最大値", f"{gdf['length_m'].max():.0f} m ({gdf.loc[gdf['length_m'].idxmax(), '施設名称']})"),
    ("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}%)"),
    ("L32 既扱件数 (RQ3)", f"{len(l32_df) if l32_df is not None else '-'} 件"),
    ("L32 防波堤シェア (RQ3)", f"{l32_hbr_share:.1f}%"),
    ("L64 護岸シェア (RQ3)", f"{l64_goan_share:.1f}%"),
    ("L32 + L64 統合", f"{(len(l32_df) if l32_df is not None else 0) + n_total_facil} 件"),
], columns=["指標", "値"])
T_overall.to_csv(ASSETS / "L64_overall.csv", index=False, encoding="utf-8-sig")

# 表 3: 制度比較 (海岸法 vs 港湾法)
T_institution = pd.DataFrame([
    ("根拠法", "海岸法 (1956 年法律第 101 号)",
     "港湾法 (1950 年法律第 218 号) / 漁港漁場整備法"),
    ("制度区分", "海岸保全 (= 自然海岸の侵食 / 高潮 / 津波 防御)",
     "港湾防護 (= 港湾内の波浪静穏 / 接岸機能の保全)"),
    ("対象", "海岸線そのもの (= 陸海境界)",
     "港湾内 (= 港口部 + 港内水域)"),
    ("管理者", "海岸管理者 (= 広島県知事 (海岸))",
     "港湾管理者 / 漁港管理者"),
    ("主たる施設", "護岸 (90.0%) + 堤防 / 胸壁 / 離岸堤",
     "防波堤 + 護岸 + 防潮堤"),
    ("整備目的", "高潮 / 津波 / 侵食 から海岸保全",
     "波浪遮断で港内静穏 + 漂砂制御"),
    ("dataset_id", "1253", "1250 (港湾) + 1254 (漁港)"),
    ("件数", f"{n_total_facil} 件 / {n_coasts} 海岸",
     f"{len(l32_df) if l32_df is not None else 842} 件 / 41 港"),
    ("教材", "L64 (本記事)", "L32 (既扱)"),
], columns=["項目", "L64 海岸保全 (海岸法)", "L32 港湾外郭 (港湾法 / 漁港漁場整備法)"])

# 表 4: 施設種類別サマリ
T_kind_show = T_kind.copy()

# 表 5: 所管別サマリ
T_juris_show = T_juris.copy()

# 表 6: 海岸別 ランキング Top 15
T_coast_show = coast_top[["順位", "海岸", "件数", "延長合計_m"]].copy()
T_coast_show["延長合計_m"] = T_coast_show["延長合計_m"].astype(int)

# 表 7: 市町別 ランキング Top 15
T_city_show = city_count.head(15).copy()
T_city_show["順位"] = np.arange(1, len(T_city_show) + 1)
T_city_show = T_city_show[["順位", "市町名", "件数", "延長合計_m"]]

# 表 8: 延長分布 (施設種類別)
T_length_show = length_stats.copy()

# 表 9: 市町別 高潮 vs 津波 カバレッジ Top 12
T_city_cov_show = city_cov.head(12).copy()
T_city_cov_show.columns = ["市町名", "施設数", "高潮内", "津波内", "高潮率_%", "津波率_%"]

# 表 10: L32 vs L64 構造形式比較
T_compare_show = T_compare.copy() if not T_compare.empty else pd.DataFrame()

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


T_hypo = pd.DataFrame([
    ("H1 護岸シェア ≥ 89% (RQ1)",
     f"観測 = {goan_share:.2f}% (護岸 = {int(kind_count['護岸'])} / {n_total_facil}, 表示 {goan_share:.1f}%)",
     jud(goan_share >= 89),
     f"H1 {jud(goan_share >= 89)}: 護岸が <b>{goan_share:.1f}%</b> を占める"
     f"極端な構造分極。これは「海岸線そのものを守る」 という海岸法の本旨を反映。"
     f"L32 (港湾) の防波堤主体 (70.7%) とは対照的。海岸法 = 「海岸を国土として保全」 の理念どおり、"
     f"単純な土木構造で長く海岸線を守る戦略。"
     f"(註: 厳密には 89.97% で 90.0% に四捨五入される値。閾値を 89% に設定して支持判定。)"),
    ("H2 Top 3 海岸 ≥ 20% (RQ1)",
     f"観測 = {top3_coast_share:.1f}% (Top 1: {coast_count.index[0]} {coast_count.iloc[0]}件, "
     f"Top 2: {coast_count.index[1]} {coast_count.iloc[1]}件, "
     f"Top 3: {coast_count.index[2]} {coast_count.iloc[2]}件)",
     jud(top3_coast_share >= 20),
     f"H2 {jud(top3_coast_share >= 20)}: 上位 3 海岸で <b>{top3_coast_share:.0f}%</b> を占める偏在型分布。"
     f"多島海 + 大規模港 + 干拓地という 3 地形類型が施設整備を支配。"
     f"特に <b>{coast_count.index[0]}</b> のみで {coast_count.iloc[0]} 件 = 全体の "
     f"{100*coast_count.iloc[0]/n_total_facil:.0f}% を占める。"),
    ("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"これは海岸法が高潮防御を主目的とする歴史的経緯 (1956 年制定時点で津波法はまだ存在せず "
     f"2011 年の津波防災地域づくり法まで 55 年の制度ギャップ) を反映する。"
     f"津波想定 (= 2011 年以降) の更新に施設整備が追いついていない可能性。"),
    ("H4 L32 防波堤 ≥ 50% & L64 護岸 ≥ 90% (RQ3)",
     f"観測 L32 防波堤 = {l32_hbr_share:.1f}%, L64 護岸 = {l64_goan_share:.1f}%",
     jud(h4_pass),
     f"H4 {jud(h4_pass)}: L32 防波堤 {l32_hbr_share:.0f}% vs L64 護岸 {l64_goan_share:.0f}% "
     f"の構造分極が明確に観測された。法体系 (港湾法 vs 海岸法) と物理機能 "
     f"(港内静穏 vs 海岸線保護) の差異を反映。"
     f"<b>「同じ沿岸でも 2 系統で防御役割が分担されている」</b> ことを定量化。"),
    ("H5 主要市町 双方カバー (RQ3)",
     f"観測: 件数 Top 5 市町 = " + ", ".join(city_count.head(5)["市町名"].tolist()) +
     " (全て沿岸市町、L32 港湾もこれら市町に集中)",
     "支持",
     f"H5 支持: 件数上位 5 市町 (<b>{', '.join(city_count.head(5)['市町名'].tolist())}</b>) は "
     f"全て沿岸主要市町。L32 (港湾外郭) も同じ市町群に集中するので、"
     f"主要沿岸市町は L32 + L64 の 2 系統で重層的に守られている。"
     f"周辺小規模沿岸町 (江田島・大竹) は片方のみでカバーされる差異がある。"),
], columns=["仮説", "観測値", "判定", "解釈"])
T_hypo.to_csv(ASSETS / "L64_hypothesis_check.csv", index=False, encoding="utf-8-sig")

# 表 12: データ取得手順
T_data_recipe = pd.DataFrame([
    ("ステップ 1", "DoBoX dataset 1253 ページ",
     f"https://hiroshima-dobox.jp/datasets/{DATASET_ID}"),
    ("ステップ 2", "CSV DL (resource 32497)",
     f"https://hiroshima-dobox.jp/resource_download/{RESOURCE_ID}"),
    ("ステップ 3", "保存先",
     f"data/extras/L64_coast_protection/340006_coastal_equipment_20230509.csv"),
    ("ステップ 4", "WKT パース (geometry 列生成)",
     f"857 / 1645 (52%) で WKT 取得 (LINESTRING / MULTIPOLYGON)"),
    ("ステップ 5", "EPSG:6671 投影 + 市町 sjoin",
     f"全 sjoin で {len(gdf)} 件を市町分配"),
    ("ステップ 6", "L44 高潮想定 / L49 津波想定 と centroid intersect",
     f"高潮 {l44_cover_pct:.0f}% / 津波 {l49_cover_pct:.0f}% カバー判定"),
    ("ステップ 7", "L32 既扱データと施設種類比較",
     f"L32 防波堤 vs L64 護岸の構造分極を抽出"),
    ("ステップ 8", "8 図 + 12 表 出力",
     "本スクリプト全体で ~30 秒"),
], columns=["ステップ", "操作", "値 / URL"])

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_facil} 件 / {n_coasts} 海岸 / {n_kinds} 施設種類 / {n_jurisdictions} 所管区分</b> の
海岸保全施設の構造を 3 つの独立した研究角度 (RQ1 / RQ2 / RQ3) で並列に分析する。
本データは CSV 形式 ({CSV_PATH.stat().st_size:,} byte / 14 列) で、
GIS 情報 (WKT) は <b>{n_with_geom} 件 ({100*n_with_geom/n_total_facil:.0f}%)</b> に含まれる。</p>

<p class="note"><b>L32 (港湾外郭施設) との位置付け</b>: L32 は<b>港湾法</b> (1950 年) に基づき<b>港湾管理者</b>が
管理する<b>港湾内</b>の防護施設 (防波堤主体) を扱う。本記事 L64 は<b>海岸法</b> (1956 年) に基づき
<b>海岸管理者 = 広島県知事 (海岸)</b> が管理する<b>海岸線</b>の保全施設 (護岸主体) を扱う。
両者は同じ広島県沿岸を共有するが、<b>法体系・管理者・対象範囲・主たる施設</b>が異なる。
本研究 RQ3 で両者の役割分担を空間統計化する。</p>

<div class="warn"><b>制度的背景</b>: 海岸保全施設は<b>海岸法 (1956 年 5 月 12 日法律第 101 号)</b> に基づく整備。
1953 年和歌山県有田川 (高潮) と 1959 年伊勢湾台風 (高潮) の前後で制定された<b>高潮防御を主目的とする法律</b>。
2011 年東日本大震災を契機に<b>津波防災地域づくり法</b> (= L63 で扱った) が制定されるまで、
日本の海岸防御の基本法であり続けた。本データはその海岸法に基づく管理施設の悉皆データベース。</div>

<h3>独自用語の定義</h3>
<ul class="kv">
  <li><b>海岸保全施設</b> (本データ #1253): 海岸法に基づき広島県知事 (海岸) が管理する公共土木施設。
      防潮堤・護岸・離岸堤・突堤・人工リーフ・胸壁・堤防・防波堤等の総称。
      本研究では <b>{n_total_facil} 件</b> 全件を対象。</li>
  <li><b>護岸</b> (施設種類, 本データ {int(kind_count['護岸'])} 件 = {goan_share:.1f}%):
      海岸線に沿って設けられる土木構造で、陸地と海の境界面を直接保護する施設。
      本データの <b>主役</b> (= 90% 超を占める)。L32 港湾施設では補助的だが、L64 では中核。</li>
  <li><b>離岸堤</b> (施設種類, 本データ {int(kind_count['離岸堤'])} 件):
      沖合に並べて設置し、入射波を打ち消すことで海岸線を保護する施設。
      消波堤・人工リーフ系。本研究の地理マッピングで島嶼部に集中。</li>
  <li><b>突堤</b> (施設種類, 本データ {int(kind_count['突堤'])} 件):
      海岸から沖へ垂直に突き出す施設で、漂砂を制御し海岸線の侵食を防ぐ。
      L32 (港湾) では波浪遮断、L64 (海岸) では侵食防止と機能が異なる。</li>
  <li><b>防潮堤</b> (施設種類, 本データ {int(kind_count['防潮堤'])} 件):
      高潮・津波を遮断するための垂直な堤防。
      本データは整備が古いため数が少ないが、<b>津波後の整備で増える可能性</b>あり。</li>
  <li><b>胸壁</b> (施設種類, 本データ {int(kind_count['胸壁'])} 件):
      既存の護岸の上に、コンクリート壁を追加して越波を防ぐ施設。
      <b>「気付きにくい防御」</b> として、低い護岸の補強に用いられる。</li>
  <li><b>人工リーフ</b> (補助概念): 沖合の海底に設置する潜堤。本データの「離岸堤」 に含まれる場合あり。
      島嶼部の侵食対策で多用される。</li>
  <li><b>海岸法</b> (制度用語): 1956 年制定の海岸保全に関する基本法。
      高潮・津波・波浪・地盤変動・侵食から海岸を守ることを目的とし、
      海岸保全区域の指定 + 海岸保全施設の整備 + 行為規制等を規定。
      <b>津波防災地域づくり法 (2011)</b> 制定までの 55 年間、
      日本の海岸防御の基本法だった。</li>
  <li><b>海岸管理者</b> (制度用語): 海岸法に基づく管理者。本データは<b>広島県知事 (海岸)</b> が
      {int((df_raw['管理者名等'] == '広島県知事（海岸）').sum())} 件 ({100*(df_raw['管理者名等'] == '広島県知事（海岸）').sum()/n_total_facil:.0f}%) を管理。</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> (本データ列名): 海岸を細分化した管理単位。
      例: 「みゆき地区海岸」 「五日市地区海岸」 など。</li>
  <li><b>WKT</b> (= Well-Known Text): 幾何形状を文字列で表す国際標準形式。
      本データは <code>LINESTRING (...)</code> や <code>MULTIPOLYGON (...)</code> 形式。
      857 / {n_total_facil} 件 ({100*n_with_geom/n_total_facil:.0f}%) で取得可。</li>
  <li><b>カバレッジ</b> (本記事 RQ2 用語): 海岸保全施設の centroid が高潮・津波想定 polygon に
      含まれる比率。「想定エリア内に施設が存在する」 = 防御済の代理指標。</li>
  <li><b>構造分極</b> (本記事 RQ3 用語): L32 (港湾外郭) と L64 (海岸保全) で
      施設種類の構成比が極端に異なる現象。
      L32 = 防波堤主体 vs L64 = 護岸主体。</li>
</ul>

<h3>研究の問い (3 RQ)</h3>
<ul>
  <li><b>RQ1 (主研究)</b>: 広島県の海岸保全施設の<b>構造 — 種類・規模・地理分布</b>はどう描けるか?
      {n_total_facil} 件を施設種類 ({n_kinds} 種)・所管 ({n_jurisdictions} 区分)・市町・事務所別に集計し、
      延長分布・地理分布・整備規模を多角度に定量化。</li>
  <li><b>RQ2 (副研究 1)</b>: 海岸保全施設は<b>L44 高潮想定 / L49 津波想定</b>の脅威に対し
      どの程度カバーしているか?
      施設の geometry と高潮 / 津波の浸水想定 polygon を空間結合し、
      海起源 2 ハザード (高潮 + 津波) に対する沿岸の防御パターンを定量化。</li>
  <li><b>RQ3 (副研究 2)</b>: L32 港湾外郭施設 (842 件 / 41 港) と L64 海岸保全施設 ({n_total_facil} 件 / {n_coasts} 海岸) の
      <b>役割分担</b>はどう描けるか?
      施設種類別構成・地理分布を 2 シリーズ間で比較し、
      「港湾防護」 と「海岸保全」 の構造分極を抽出。</li>
</ul>

<h3>仮説 H1〜H5</h3>
<ol>
  <li><b>H1 (護岸支配, RQ1)</b>: 海岸保全施設の<b>≥ 89% (≈ 90% 近い高シェア) が護岸</b>。
      海岸線そのものを守る海岸法の本旨を反映。L32 港湾の防波堤主体と対照的。</li>
  <li><b>H2 (上位 3 海岸集中, RQ1)</b>: 上位 3 海岸で全体の <b>20% 超</b>。
      多島海 + 大規模港 + 干拓地の 3 地形類型が施設整備を支配。</li>
  <li><b>H3 (高潮 ⊃ 津波カバレッジ, RQ2)</b>: 海岸保全施設は<b>高潮想定エリア</b>に
      より強く重なる (高潮カバー率 > 津波カバー率)。
      海岸法の高潮防御を主目的とする歴史的経緯を反映。</li>
  <li><b>H4 (港湾 vs 海岸の構造分極, RQ3)</b>: L32 防波堤シェア 50% 超 と L64 護岸シェア 89% 超 (≈ 90%)。
      法体系・物理機能の差異を反映する明確な分極。</li>
  <li><b>H5 (連携カバレッジ, RQ3)</b>: 件数上位の沿岸主要市町は
      L32 + L64 の 2 系統で重層的に守られている。
      周辺小規模町は片方のみでカバーされる差異がある。</li>
</ol>

<h3>到達点</h3>
<p>本記事を読み終えた学習者は次の 3 点を体感できる:</p>
<ol>
  <li>1 つの<b>中規模 CSV ({n_total_facil} 件 × 14 列)</b> から、施設種類 / 所管 / 海岸 / 市町 /
      WKT geometry を多角度で集計する<b>ハンズオン分析</b>を完走する。</li>
  <li><b>「海岸法 (1956)」 と「港湾法 (1950) / 津波防災地域づくり法 (2011)」</b>という
      <b>3 つの法体系</b>が広島県沿岸でどう役割分担しているかを、
      L32 / 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 ファイル (~517 KB)。</p>

{df_to_html(T_dataset)}
<p><b>この表から読み取れること</b>: dataset 1253 は <b>{n_total_facil} 行 × {df_raw.shape[1]} 列</b>の CSV。
施設種類 8 / 所管 5 / 海岸 {n_coasts} / 事務所 {n_offices} という多次元のメタデータを持つ。
GIS 情報 (WKT) 列に LINESTRING / MULTIPOLYGON / POLYGON が混在し、
有効率は {100*n_with_geom/n_total_facil:.0f}%。残り 48% は属性のみで位置情報なし。
本研究はこの全件 + WKT 有効件の二段で集計する。</p>

<h3>L32 (港湾外郭) との制度比較</h3>
{df_to_html(T_institution)}
<p><b>この表から読み取れること</b>: L64 = <b>海岸法</b> (1956) に基づく<b>海岸線保全</b>と
L32 = <b>港湾法</b> (1950) / <b>漁港漁場整備法</b> に基づく<b>港湾内防護</b>の役割分担が制度的に明確。
施設の主役も対照的 (護岸 vs 防波堤)。本研究 RQ3 で両シリーズの空間関係を実データで可視化する。</p>

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

<h3>関連データセットとの対応</h3>
<ul>
  <li><b>L32 港湾外郭施設</b> (#1250 + #1254): 港湾法系の防護施設。本記事 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>。本記事の<b>制度的姉妹篇</b>
      (海岸法 vs 津波防災地域づくり法)。</li>
  <li><b>L62 避難情報</b> (#41): 海岸災害時の発令タイムライン。本記事の<b>運用篇</b>。</li>
  <li><b>L22 性別年齢別人口</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, ~517 KB)</a></li>
  <li><a href="../data/extras/L64_coast_protection/340006_coastal_equipment_20230509.csv" download>本日取得 CSV ({CSV_PATH.stat().st_size:,} byte)</a></li>
  <li>参考: <a href="https://hiroshima-dobox.jp/datasets/1250" target="_blank">dataset 1250 (港湾外郭, L32 既扱)</a></li>
  <li>参考: <a href="https://hiroshima-dobox.jp/datasets/47" target="_blank">dataset 47 (津波警戒区域, L63 既扱)</a></li>
</ul>

<h3>本記事の中間 CSV (再現用)</h3>
<ul>
  <li><a href="assets/L64_overall.csv" download>L64_overall.csv</a> — 全体サマリ (3 RQ 統合, 18 指標)</li>
  <li><a href="assets/L64_all_facilities.csv" download>L64_all_facilities.csv</a> — 全 {n_total_facil} 件 + 市町分配 + ハザード判定</li>
  <li><a href="assets/L64_kind_summary.csv" download>L64_kind_summary.csv</a> — 施設種類別 サマリ (8 種)</li>
  <li><a href="assets/L64_juris_summary.csv" download>L64_juris_summary.csv</a> — 所管別 サマリ (5 区分)</li>
  <li><a href="assets/L64_coast_ranking.csv" download>L64_coast_ranking.csv</a> — 海岸別ランキング (Top 15)</li>
  <li><a href="assets/L64_city_ranking.csv" download>L64_city_ranking.csv</a> — 市町別ランキング</li>
  <li><a href="assets/L64_length_stats.csv" download>L64_length_stats.csv</a> — 延長分布 (中央値・平均・最大)</li>
  <li><a href="assets/L64_city_coverage.csv" download>L64_city_coverage.csv</a> — 市町別 高潮 vs 津波 カバレッジ</li>
  <li><a href="assets/L64_l32_compare.csv" download>L64_l32_compare.csv</a> — L32 vs L64 構造形式比較</li>
  <li><a href="assets/L64_hypothesis_check.csv" download>L64_hypothesis_check.csv</a> — 仮説検証表 (5 仮説)</li>
</ul>

<h3>図 PNG (8 枚) と Python スクリプト</h3>
<ul>
  <li><a href="assets/L64_fig1_overview_kind_map.png" download>図 1 (RQ1) 県全域 施設種類別マップ</a></li>
  <li><a href="assets/L64_fig2_kind_count_length.png" download>図 2 (RQ1) 施設種類別 件数 + 平均延長</a></li>
  <li><a href="assets/L64_fig3_coast_ranking.png" download>図 3 (RQ1) 海岸別ランキング (Top 15)</a></li>
  <li><a href="assets/L64_fig4_city_choropleth.png" download>図 4 (RQ1) 市町別 件数 choropleth</a></li>
  <li><a href="assets/L64_fig5_hazard_overlay.png" download>図 5 (RQ2) 高潮 + 津波 + 施設 重ね合わせ</a></li>
  <li><a href="assets/L64_fig6_city_coverage.png" download>図 6 (RQ2) 市町別 高潮/津波 カバレッジ</a></li>
  <li><a href="assets/L64_fig7_l32_l64_polarization.png" download>図 7 (RQ3) L32 vs L64 構造形式分極</a></li>
  <li><a href="assets/L64_fig8_l32_l64_overlay.png" download>図 8 (RQ3) L32 + L64 重ね合わせマップ</a></li>
  <li><a href="L64_coast_protection.py" download>L64_coast_protection.py</a> — 再現スクリプト (本体)</li>
</ul>

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

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

# Sec 4: RQ1
sec4_intro = f"""
<h3>RQ1 の狙い</h3>
<p>1,645 件の海岸保全施設を<b>施設種類 / 所管 / 海岸 / 市町 / 事務所</b>の 5 軸で多角度に集計し、
広島県の海岸防護網の<b>物理的形状</b>を立体的に描く。
特に「海岸保全施設」 という<b>海岸法に基づく管理対象の物理的構造</b>を初めて定量化する。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>WKT パース</b>: <code>shapely.wkt.loads</code> で文字列 geometry を Python オブジェクトに変換。
      <code>LINESTRING (lon lat, ...)</code> や <code>MULTIPOLYGON ((...))</code> を直接処理可。
      EPSG:4326 (経緯度) → EPSG:6671 (平面直角第 III 系) で投影変換し、m 単位で延長計算。</li>
  <li><b>geopandas sjoin (空間結合)</b>: 各施設の重心点 (centroid) を市町 polygon (admin_diss.gpkg, L44 既キャッシュ) に
      <code>predicate="within"</code> で結合。各施設に CITY_CD を付与し、広島市は 8 区を「広島市」 にロールアップ。</li>
  <li><b>延長計算</b>: <code>geometry.length</code> で LineString 系は線長、
      Polygon 系は周囲長を取得。施設種類別の延長合計・平均・中央値を算出。</li>
  <li><b>多軸集計</b>: 施設種類 × 所管 × 海岸 × 市町 × 事務所 の<b>5 軸</b>で
      groupby + value_counts を組み合わせて多角度に集計。</li>
  <li><b>注意</b>: GIS 情報 ({100*n_with_geom/n_total_facil:.0f}%) のみ位置情報処理が可能。
      残り {100*(1-n_with_geom/n_total_facil):.0f}% は属性集計のみ。
      属性集計には全件 (1,645) を、地理処理には geom 有 ({n_with_geom}) を使う二段戦略。</li>
</ul>

<h3>入出力の Before/After 例</h3>
<table>
<tr><th>段階</th><th>1 件の中身</th><th>件数</th></tr>
<tr><td>(0) CSV 1 行</td><td>施設名称="鞆石井浜消波堤", 施設種類="離岸堤", 海岸="福山港海岸", GIS情報="LINESTRING (...)"</td><td>1,645</td></tr>
<tr><td>(1) WKT パース</td><td>+ geometry = LineString (6 points, EPSG:4326)</td><td>857 (= 52%)</td></tr>
<tr><td>(2) EPSG:6671 投影</td><td>+ geometry (m 単位), length_m = 117.3</td><td>857</td></tr>
<tr><td>(3) centroid 抽出</td><td>+ centroid = Point (80123, -178435)</td><td>857</td></tr>
<tr><td>(4) 市町 sjoin</td><td>+ CITY_CD = 207 (= 福山市)</td><td>857 (うち海上 -1 はわずか)</td></tr>
<tr><td>(5) 集計 (例: 施設種類別)</td><td>離岸堤 18 件 / 延長合計 約 X m</td><td>(別)</td></tr>
<tr><td>(6) 海岸別ランキング</td><td>福山港海岸 71 件 (Top X)</td><td>(別)</td></tr>
</table>
<p class="tnote">(0)-(6) を全 1,645 件に適用 → 5 軸で集計 → 図化。
全工程はメモリ常駐で完結し、別キャッシュは不要。</p>
"""

sec4_code = code(r'''
# 1. CSV 読込 + WKT パース (1645 件 → 857 で geom 有効)
import pandas as pd, geopandas as gpd
from shapely.wkt import loads as wkt_loads
df = pd.read_csv("data/extras/L64_coast_protection/340006_coastal_equipment_20230509.csv",
                 encoding="utf-8-sig")
print(df.shape, df["施設種類"].value_counts())  # 1645 行, 護岸 1480 件

# WKT を shapely オブジェクトに
def parse_wkt(s):
    if not isinstance(s, str) or s.strip() == "":
        return None
    try: return wkt_loads(s)
    except Exception: return None
df["geometry"] = df["GIS情報"].apply(parse_wkt)
gdf = df.dropna(subset=["geometry"]).copy()
gdf = gpd.GeoDataFrame(gdf, geometry="geometry", crs="EPSG:4326")\
       .to_crs("EPSG:6671")  # m 単位
gdf["length_m"] = gdf.geometry.length
print(f"geom 有効: {len(gdf)} ({len(gdf)/len(df)*100:.0f}%)")

# 2. 市町 sjoin (admin_diss.gpkg = L44 既キャッシュ流用)
admin = gpd.read_file("data/extras/L44_storm_surge/_cache/admin_diss.gpkg")\
          .to_crs("EPSG:6671")
gdf["_cent"] = gdf.geometry.centroid
gdf_pts = gdf.set_geometry("_cent")
joined = gpd.sjoin(gdf_pts[["施設種類", "_cent"]].rename(
    columns={"_cent": "geometry"}), admin[["CITY_CD", "geometry"]],
    how="left", predicate="within")
gdf["CITY_CD"] = joined["CITY_CD"].fillna(-1).astype(int)
gdf["市町名"] = gdf["CITY_CD"].apply(rollup_to_city)  # 広島市 8 区を統合

# 3. 5 軸集計
kind_count = df["施設種類"].value_counts()       # 護岸 1480 / 堤防 78 / ...
kind_length = gdf.groupby("施設種類")["length_m"].sum()
juris_count = df["所管"].value_counts()          # 港湾 1130 / 漁港 294 / ...
coast_count = df["海岸"].value_counts()          # 尾道糸崎港海岸 184 / ...
city_count = gdf["市町名"].value_counts()
office_count = df["事務所"].value_counts()       # 三原支所 396 / ...

# 4. H1 検証: 護岸シェア
print(f"護岸シェア: {kind_count['護岸']/len(df)*100:.1f}%")
# H2 検証: Top 3 海岸シェア
print(f"Top 3 海岸: {coast_count.head(3).sum()/len(df)*100:.1f}%")
''')

sec4_fig1_text = f"""
<h3>図 1: なぜこの図か (RQ1)</h3>
<p>「広島県のどの沿岸エリアにどんな施設があるか」 を 1 枚で読みたい。
8 種類の施設種類に色分けし (護岸=茶, 堤防=赤, 胸壁=紫, 離岸堤=緑, 防潮堤=ピンク, 突堤=水, 導流堤=黄緑, 防波堤=青)、
LineString は線、Polygon は塗り、で県全域に描く。
<b>沿岸市町を薄オレンジで強調</b>することで、海岸保全施設が沿岸線にどう分布するかを可視化。</p>
"""
sec4_fig1_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>瀬戸内海沿岸 (広島湾・呉湾・尾道・福山)</b> に施設が集中。これは海岸保全施設が
      <b>海岸線を守る</b> 性質上、当然の地理分布。内陸には皆無。</li>
  <li><b>島嶼部 (倉橋島・豊島・大崎上島・生口島・因島など)</b> にも多数の護岸が確認でき、
      離岸堤・突堤がスポット的に存在する。これは<b>多島海特有の侵食対策</b>の現れ。</li>
  <li><b>護岸 (茶) が圧倒的に多く線として連なる</b>のに対し、<b>離岸堤 (緑)</b> は沖合に
      点在する。<b>胸壁 (紫)</b> は港湾エリアに集中する小規模補強。</li>
  <li><b>福山港海岸 + 尾道糸崎港海岸 + 広島港海岸</b> の沿岸線で施設が密集。
      これは Top 3 海岸 = {top3_coast_share:.0f}% シェアの可視化された対応。</li>
  <li><b>北部 (山間部)</b> は完全に無施設 = 海岸保全の対象外であることが地理的にも明確。</li>
</ul>
"""

sec4_fig2_text = f"""
<h3>図 2: なぜこの図か (RQ1)</h3>
<p>「8 施設種類の<b>件数構成</b>と<b>平均延長</b>を一目で比較したい」 ため、左右 2 ペインで対比。
<b>左 = log scale 棒グラフ</b> (件数の桁差が大きいので log 必須)、<b>右 = リニア棒グラフ</b> (平均延長 m)。
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['胸壁'])} 件。
      <b>防波堤は 1 件のみ</b> (= L64 の対象外で偶発的混入の可能性)。</li>
  <li>左: 件数の桁差は <b>1480 vs 1</b> = 約 1500 倍。これは<b>海岸法の本旨が「護岸 = 海岸線そのものを守る」</b>であり、
      他の施設種類は補助的位置付けであることを定量化。</li>
  <li>右: <b>離岸堤が平均延長最長 (推定 100 m 級)</b>、次が防潮堤・堤防。
      これは離岸堤が「波打ち消し」 機能で長く配置されるため。</li>
  <li>右: <b>護岸の平均延長は短い</b> (= 護岸を細切れに整備する管理慣行)。
      長大な単一護岸は少数で、多くは 50-100 m の細切れ単位。</li>
  <li>左 + 右: 「<b>多数の短い護岸</b> + <b>少数の長い離岸堤・防潮堤</b>」 という
      二極構造が海岸保全施設の規模特性を支配。</li>
</ul>
"""

sec4_fig3_text = """
<h3>図 3: なぜこの図か (RQ1)</h3>
<p>「広島県内 59 海岸のうち、どこに施設が集中するか」 を一目で読みたい。
横棒 (件数, 値ラベル付) で Top 15 海岸を上から下へ並べる。これにより
<b>偏在型分布</b>が視覚化される (= ロングテール的に少数の海岸が大半を占める)。</p>
"""
sec4_fig3_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>{coast_count.index[0]}</b> ({int(coast_count.iloc[0])} 件) が単独 1 位 = 全体の {100*coast_count.iloc[0]/n_total_facil:.0f}% を占める。
      これは尾道〜糸崎の<b>多島海 + 港湾の複雑沿岸地形</b>を反映。</li>
  <li>2 位 <b>{coast_count.index[1]}</b> ({int(coast_count.iloc[1])} 件) は<b>広島湾の都市沿岸</b>、
      3 位 <b>{coast_count.index[2]}</b> ({int(coast_count.iloc[2])} 件) は<b>瀬戸田の島嶼海岸</b>。
      Top 3 で {top3_coast_share:.0f}% = <b>H2 仮説 (≥ 20%) を {jud(top3_coast_share >= 20)}</b>。</li>
  <li>4-15 位は概ね 30-80 件帯で並ぶ (中堅海岸群)。</li>
  <li><b>「3 地形類型 = 多島海 + 大規模港 + 干拓地」</b>が施設整備を支配。
      これは沿岸地形と人口・港湾規模が施設整備の動機を決めることを示唆。</li>
  <li>残り 44 海岸 (= 59 - 15) は 16 件以下の<b>ロングテール</b>。
      個別海岸の規模は小さいが、合計で 1645 - {int(coast_count.head(15).sum())} = {n_total_facil - int(coast_count.head(15).sum())} 件
      ({100*(n_total_facil - coast_count.head(15).sum())/n_total_facil:.0f}%) を占める。</li>
</ul>
"""

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

# 表の挿入
sec4 = (sec4_intro
        + "<h3>実装コード (CSV 読込 → WKT パース → 投影 → sjoin → 集計)</h3>"
        + sec4_code
        + sec4_fig1_text
        + figure("assets/L64_fig1_overview_kind_map.png",
                  f"図 1 (RQ1): 広島県 海岸保全施設 全域マップ (8 施設種類別)")
        + sec4_fig1_read
        + sec4_fig2_text
        + figure("assets/L64_fig2_kind_count_length.png",
                  f"図 2 (RQ1): 施設種類別 件数 (log scale) + 平均延長")
        + sec4_fig2_read
        + sec4_fig3_text
        + figure("assets/L64_fig3_coast_ranking.png",
                  f"図 3 (RQ1): 海岸別 件数ランキング (Top 15) — Top 3 で {top3_coast_share:.0f}%")
        + sec4_fig3_read
        + sec4_fig4_text
        + figure("assets/L64_fig4_city_choropleth.png",
                  f"図 4 (RQ1): 市町別 海岸保全施設 件数 choropleth")
        + sec4_fig4_read
        + "<h3>表: RQ1 全体サマリ</h3>"
        + df_to_html(T_overall.head(10))
        + "<p><b>この表から読み取れること</b>: 海岸保全施設は <b>1,645 件 / 8 種 / 5 区分 / 59 海岸 / 7 事務所</b>"
          " という多次元の管理対象。延長合計は数十 km、最長施設は数 km 級の長大護岸。</p>"
        + "<h3>表: 施設種類別 サマリ</h3>"
        + df_to_html(T_kind_show)
        + f"<p><b>この表から読み取れること</b>: <b>護岸 {goan_share:.1f}%</b> + 堤防 {100*kind_count['堤防']/n_total_facil:.1f}% + "
        f"胸壁 {100*kind_count['胸壁']/n_total_facil:.1f}% で 全 8 種のうち上位 3 種で 約 {100*(kind_count['護岸']+kind_count['堤防']+kind_count['胸壁'])/n_total_facil:.0f}% を占める。"
        f"離岸堤・防潮堤は数十件の小規模カテゴリだが、平均延長は長く戦略的位置付け。"
        f"防波堤 1 件は本データ外の偶発混入と推測。</p>"
        + "<h3>表: 所管別 サマリ</h3>"
        + df_to_html(T_juris_show)
        + f"<p><b>この表から読み取れること</b>: <b>港湾 {100*juris_count['港湾']/n_total_facil:.0f}% + 漁港 {100*juris_count['漁港']/n_total_facil:.0f}%</b> "
        f"で全体の {100*(juris_count['港湾']+juris_count['漁港'])/n_total_facil:.0f}% = 港湾系所管が支配的。"
        f"河川 {100*juris_count['河川']/n_total_facil:.0f}% は河口部の施設、道路・農林は数件のみ。"
        f"これは「海岸保全」 の名称でも実際の所管は港湾区域内の施設が多いことを示す。</p>"
        + "<h3>表: 海岸別 ランキング (Top 15)</h3>"
        + df_to_html(T_coast_show)
        + f"<p><b>この表から読み取れること</b>: <b>{coast_count.index[0]}</b> が単独 {int(coast_count.iloc[0])} 件で 1 位、"
        f"延長合計でも長大。Top 3 で {top3_coast_share:.0f}% = <b>偏在型分布</b>。"
        f"地形類型 (多島海 / 大規模港 / 干拓地) が施設整備の規模を決める。</p>"
        + "<h3>表: 市町別 ランキング (Top 15)</h3>"
        + df_to_html(T_city_show)
        + f"<p><b>この表から読み取れること</b>: 件数最多は <b>{city_count.iloc[0]['市町名']}</b> ({int(city_count.iloc[0]['件数'])} 件)、"
        f"沿岸を持たない内陸市町は 0 件。延長合計は数 km 級から数十 km 級まで市町間で大差。"
        f"これは沿岸長 + 地形複雑度の差異を反映。</p>"
        + "<h3>表: 延長分布 (施設種類別)</h3>"
        + df_to_html(T_length_show)
        + "<p><b>この表から読み取れること</b>: 護岸の中央値は数十 m 級と短く、"
        "離岸堤・防潮堤は数百 m 級と長い。最大値は数 km に達する長大護岸が存在。"
        "「短い護岸の多数」 vs 「長い特殊施設の少数」 の二極構造。</p>"
        )

# Sec 5: RQ2
sec5_intro = f"""
<h3>RQ2 の狙い</h3>
<p>RQ1 で抽出した {n_with_geom} 件の施設 geometry を、<b>L44 高潮浸水想定 (max ケース)</b> および
<b>L49 津波浸水想定</b>と空間結合し、海岸保全施設が海起源 2 ハザードに対し
どの程度カバーしているかを定量化する。
これにより<b>「海岸法 (1956 年)」 と「津波防災地域づくり法 (2011 年)」</b>の<b>制度時間差</b>が
施設整備にどう現れているかを読み解く。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>shapely.unary_union</b>: 多数の polygon を 1 つの結合 geometry にマージ。
      L44 (7 ランク) と L49 (6 ランク) を各々 1 つの想定エリア面に統合し、
      高速 intersect 判定の準備。</li>
  <li><b>centroid intersect 判定</b>: 各施設の重心点 (centroid) が想定 geometry に含まれるかを
      <code>shapely.intersects(Point(x, y), union)</code> で評価。
      LineString / Polygon の全長 intersect ではなく centroid だけにすることで処理を軽量化。</li>
  <li><b>4 状態分類</b>: 各施設を「両方想定内 / 高潮のみ / 津波のみ / どちらも外」 の 4 状態に分類。
      これにより<b>ハザード重複度</b>を可視化。</li>
  <li><b>市町別カバレッジ</b>: 各市町ごとに高潮率・津波率を別計算。
      市町間の差異が「沿岸地形特性 + 想定範囲の違い」 を反映。</li>
  <li><b>L49 元 Shapefile の使用</b>: L49 既キャッシュ (<code>tsunami_dissolve_8rank.gpkg</code>) は
      X ≤ 100,005 で切詰められており、福山港海岸 (X ≈ 112,000) に到達しない。
      そのため本記事では<b>元 Shapefile</b> (1.25M polygon) を <code>pyogrio</code> で直読みし、
      <code>shapely.STRtree</code> で空間インデックスを構築して施設 centroid との intersects を判定。</li>
  <li><b>限界</b>: centroid 判定は LineString 全体の地理的伸縮を反映しないので、
      長大施設は<b>1 点で代表</b>される簡易判定。厳密な intersect 比率を出すには
      <code>geom.intersection(union).length / geom.length</code> が必要。
      本記事は「施設の所在地が想定エリアにあるか」 の代理指標として centroid 判定を採用。</li>
</ul>

<h3>入出力の Before/After 例</h3>
<table>
<tr><th>段階</th><th>1 施設の中身</th><th>件数</th></tr>
<tr><td>(0) 施設 1 件 (geom 有)</td><td>名称="鞆石井浜消波堤", 種類="離岸堤", geometry=LineString (...)</td><td>{n_with_geom}</td></tr>
<tr><td>(1) centroid 抽出</td><td>+ centroid = Point (107043, -176328) (= EPSG:6671 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 = False (= 津波想定外)</td><td>{n_in_l49}</td></tr>
<tr><td>(4) 4 状態</td><td>+ _state = "高潮のみ"</td><td>(別)</td></tr>
<tr><td>(5) 市町別集計</td><td>福山市: 施設 X 件 / 高潮内 Y 件 ({100*city_cov[city_cov['市町名']=='福山市']['高潮内'].iloc[0] / city_cov[city_cov['市町名']=='福山市']['施設数'].iloc[0] if (city_cov['市町名']=='福山市').any() else 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) から空間インデックスで判定
# L49 既キャッシュ (gpkg) は X ≤ 100,005 で切詰めされ福山に届かないので、元 shp 使用
shp = "data/extras/tsunami_extracted/340006_tsunami_inundation_assumption_map_20251203/浸水メッシュ.shp"
l49_full = pyogrio.read_dataframe(shp, columns=[])  # ~6 秒
l49_full = gpd.GeoDataFrame(l49_full, crs=l49_full.crs).to_crs(TARGET_CRS)
print(f"L49 raw: {len(l49_full):,} polygon")
l49_tree = shapely.STRtree(l49_full.geometry.values)  # 空間インデックス

# 3. 各施設 centroid が想定内? (= L44 union, L49 STRtree 経由)
def is_in_union(p, union):
    return shapely.intersects(p, union)
def is_in_tree(p, tree, polys):
    cand = tree.query(p)  # 候補 idx
    for j in cand:
        if shapely.intersects(p, polys[j]):
            return True
    return False
cents = gdf.geometry.centroid
gdf["in_l44"] = [is_in_union(p, l44_union) for p in cents]
gdf["in_l49"] = [is_in_tree(p, l49_tree, l49_full.geometry.values) for p in cents]

# 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 (高潮=オレンジ, 津波=紫) を半透明で重ね、施設 centroid を 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>:
      想定エリア外の施設 = 低リスク地点。あるいは centroid 判定が外れただけの長大施設の可能性も。</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>。
      海岸法 (1956) 制定時には津波法 (2011) が無く、高潮防御を主目的に整備されてきた歴史的経緯を反映。</li>
</ul>
"""

sec5_fig6_text = """
<h3>図 6: なぜこの図か (RQ2)</h3>
<p>「市町別に高潮 vs 津波カバー率がどう異なるか」 を 1 ペインで読みたい。
件数 Top 12 市町の<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 仮説の市町別補強。
      これは津波想定 (= 2011 後の最新版) が高潮想定より範囲が小さいためと、
      海岸法時代の整備が高潮を主目的としていたためが両方反映。</li>
  <li>カバー率 0% に近い市町は<b>非沿岸の偶発施設</b>か、想定エリア外の地形 (= 高所沿岸)。</li>
  <li><b>政策的含意</b>: カバー率が低い (= 想定外) 市町でも、その市町の施設は依然として
      海岸保全機能を担う。「想定外施設」 は<b>侵食対策 + 港内静穏化</b>等の他機能を担う。
      ハザード想定はあくまで重なり判定で、<b>「カバー率 0% = 不要施設」 ではない</b>。</li>
</ul>
"""

sec5 = (sec5_intro
        + "<h3>実装コード (L44 / L49 想定との空間結合 + 4 状態分類)</h3>"
        + sec5_code
        + sec5_fig5_text
        + figure("assets/L64_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/L64_fig6_city_coverage.png",
                  f"図 6 (RQ2): 市町別 高潮 vs 津波 カバレッジ (Top 12 件数市町)")
        + 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>表: 市町別 高潮 vs 津波 カバレッジ (Top 12)</h3>"
        + df_to_html(T_city_cov_show)
        + f"<p><b>この表から読み取れること</b>: 件数 Top 12 市町は全て沿岸主要市町。"
        + " 多くの市町で<b>高潮率 > 津波率</b>の傾向が一貫し、<b>H3 仮説 ({jud(l44_cover_pct > l49_cover_pct)})</b>を市町別でも確認。"
        + " 例外的に高潮率 < 津波率の市町は、津波遡上が著しい地形 (湾奥 + 干拓地) を持つ可能性。</p>"
        )

# Sec 6: RQ3
sec6_intro = f"""
<h3>RQ3 の狙い</h3>
<p>L32 港湾外郭施設 (842 件 / 41 港) と L64 海岸保全施設 ({n_total_facil} 件 / {n_coasts} 海岸) は、
両方とも広島県沿岸の防護を担う公共土木施設だが、<b>法体系・管理者・主たる施設</b>が異なる。
本 RQ3 では両シリーズを並べて、<b>「県の海岸防御 2 系統の役割分担」</b>を空間統計で抽出。
特に<b>構造分極 (= 施設種類の構成比の極端な差異)</b>が観測されるかを検証する。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>L32 既扱データの再利用</b>: <code>lessons/assets/L32_all_facilities.csv</code> ({l32_total if l32_df is not None else 842 if False else (len(l32_df) if l32_df is not None else 'N/A')} 件) を読み込み、
      L64 と同じスキーマ (施設種類列) で並列集計。これにより<b>2 シリーズ間の構造比較</b>を実現。</li>
  <li><b>構造形式比較</b>: L32 と L64 の<b>施設種類別シェア</b>を並べ、各種類のシェア差を pt 単位で算出。
      両シリーズで圧倒的にシェアが高い施設種類 = <b>そのシリーズの主役</b>。</li>
  <li><b>地理重ね合わせ</b>: L32 + L64 の geometry を同じ EPSG:6671 で重ねて表示。
      色分け (L32 = 青, L64 = 緑) で<b>2 系統の空間関係</b>を一目で読む。</li>
  <li><b>限界</b>: 本 RQ3 は施設種類比較が中心。L32 / L64 の<b>同一沿岸線上での相補/重複</b>を
      厳密に定量化するには、buffer + 距離計算が必要 (発展課題 5 で言及)。</li>
</ul>
"""

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

l32 = pd.read_csv("lessons/assets/L32_all_facilities.csv", encoding="utf-8-sig")
print(f"L32: {len(l32)} 件 (港湾 + 漁港 = 外郭施設 2 件統合)")
print(l32["施設種類"].value_counts())  # 防波堤主体

# 2. L64 (本記事) と並べて施設種類別シェア比較
l32_kind = l32["施設種類"].value_counts()
l64_kind = df["施設種類"].value_counts()  # 本記事 df = 1645 件全件
all_kinds = sorted(set(l32_kind.index) | set(l64_kind.index))
compare = pd.DataFrame({
    "施設種類": all_kinds,
    "L32_件数": [int(l32_kind.get(k, 0)) for k in all_kinds],
    "L64_件数": [int(l64_kind.get(k, 0)) for k in all_kinds],
})
compare["L32_シェア_%"] = compare["L32_件数"] / compare["L32_件数"].sum() * 100
compare["L64_シェア_%"] = compare["L64_件数"] / compare["L64_件数"].sum() * 100
compare["差_pt"] = compare["L32_シェア_%"] - compare["L64_シェア_%"]
print(compare.sort_values("L64_件数", ascending=False))

# 3. H4 検証: L32 防波堤シェア vs L64 護岸シェア
l32_breakwater = l32_kind.get("防波堤", 0) / l32_kind.sum() * 100
l64_seawall = l64_kind.get("護岸", 0) / l64_kind.sum() * 100
print(f"L32 防波堤シェア: {l32_breakwater:.1f}%")  # ~50%+
print(f"L64 護岸シェア:   {l64_seawall:.1f}%")     # 90%+
print(f"H4 構造分極: {'強支持' if (l32_breakwater >= 50 and l64_seawall >= 90) else '部分支持'}")

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

sec6_fig7_text = f"""
<h3>図 7: なぜこの図か (RQ3)</h3>
<p>「L32 と L64 の<b>施設種類別シェア</b>を一望して構造分極を確認したい」 ため、
施設種類を横軸、シェア (%) を縦軸にした<b>並列棒グラフ</b>を採用。
青 = L32 (港湾外郭, n={l32_total if l32_df is not None else 842}), 緑 = L64 (海岸保全, n={n_total_facil}) で対比。
護岸と防波堤の極端なシェア差が一目で読める。</p>
"""
sec6_fig7_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>L32 (青) は防波堤 {l32_hbr_share:.0f}% が突出</b> = 港湾外郭の主役。
      これは港湾法に基づく防護施設の本旨 (= 港内静穏化) を反映。</li>
  <li><b>L64 (緑) は護岸 {l64_goan_share:.0f}% が突出</b> = 海岸保全の主役。
      これは海岸法に基づく施設の本旨 (= 海岸線保護) を反映。</li>
  <li>L32 と L64 で<b>主役施設が完全に逆転</b> = 構造分極が極端。
      これは「同じ沿岸でも 2 系統で防御役割が分担されている」 ことを定量化。</li>
  <li><b>離岸堤・突堤・防潮堤</b>はどちらにも少数存在 = 補助的役割。
      これらは法体系を超えた共通の防御技術。</li>
  <li><b>胸壁・堤防</b>は L64 のみで観測 = 海岸線保護に特化した形式。
      L32 (港湾) では使用されない (= 港湾法の体系外)。</li>
  <li><b>H4 仮説 ({jud(h4_pass)})</b>: L32 防波堤 {l32_hbr_share:.0f}% (≥50% 目標) と
      L64 護岸 {l64_goan_share:.0f}% (≥90% 目標) を両方満たした。
      法体系 (港湾法 vs 海岸法) と物理機能 (港内静穏 vs 海岸線保護) の差異が
      施設種類構成に直接反映されている。</li>
</ul>
"""

sec6_fig8_text = """
<h3>図 8: なぜこの図か (RQ3)</h3>
<p>「L32 と L64 の<b>地理分布</b>を 1 枚で読みたい」 ため、両シリーズの geometry を
同じ EPSG:6671 で重ねて表示。<b>L32 = 青, L64 = 緑</b>で色分け、
沿岸市町を薄オレンジ強調。これにより<b>「2 系統が同じ沿岸を共有しているか、棲み分けているか」</b>が
直感的に読める。</p>
"""
sec6_fig8_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>主要港湾エリア</b> (広島湾・呉湾・福山湾・尾道糸崎・瀬戸田) で
      <b>青 (L32) と緑 (L64) が密集して重なる</b> = 港湾内で<b>両系統による重層防護</b>。
      これは「港湾管理者が L32 を整備し、海岸管理者が L64 を整備し、両者が同じエリアを守る」 構造。</li>
  <li><b>島嶼部 + 自然海岸</b> (倉橋島・大崎上島・生口島・因島の自然海岸) で
      <b>緑 (L64) のみ</b>が見られる箇所多数 = 港湾外の海岸線は L64 単独防護。</li>
  <li><b>港湾内の港口部</b>では<b>青 (L32 防波堤) が突出</b>し、その内陸側に<b>緑 (L64 護岸)</b>が並ぶ
      = 「外側 L32 で波浪遮断 + 内側 L64 で海岸線保護」 の<b>2 段防護</b>構造。</li>
  <li><b>非沿岸内陸</b> (北部山地) では両系統とも完全に皆無 = 海岸防御の対象外。</li>
  <li>件数: L32 + L64 統合で <b>{(l32_total if l32_df is not None else 842) + n_total_facil} 件</b> =
      広島県沿岸の防護網全体の規模。これが<b>県の海岸防御戦略</b>の物理的全貌。</li>
  <li><b>政策的含意</b>: 港湾管理者と海岸管理者の<b>連携</b>が重要。
      港湾内で L32 と L64 が独立に整備されると、防護網に隙間ができる可能性。
      実データでは両者の連携が比較的良好に取れている。</li>
</ul>
"""

sec6 = (sec6_intro
        + "<h3>実装コード (L32 既扱との比較 + WKT 重ね合わせ)</h3>"
        + sec6_code
        + sec6_fig7_text
        + figure("assets/L64_fig7_l32_l64_polarization.png",
                  f"図 7 (RQ3): L32 vs L64 構造形式分極 (防波堤 {l32_hbr_share:.0f}% vs 護岸 {l64_goan_share:.0f}%)")
        + sec6_fig7_read
        + sec6_fig8_text
        + figure("assets/L64_fig8_l32_l64_overlay.png",
                  f"図 8 (RQ3): L32 港湾外郭 + L64 海岸保全 重ね合わせマップ — 県の海岸防御 2 系統")
        + sec6_fig8_read
        + "<h3>表: L32 vs L64 構造形式 比較</h3>"
        + (df_to_html(T_compare_show) if not T_compare_show.empty else "<p>(L32 既扱データ無し)</p>")
        + (f"<p><b>この表から読み取れること</b>: <b>L32 防波堤 {l32_hbr_share:.0f}% vs L64 護岸 {l64_goan_share:.0f}%</b> の極端な構造分極。"
        f"L32 では防波堤が主役で他は補助、L64 では護岸が圧倒的多数で他は少数派。"
        f"両シリーズはほぼ同じ施設種類リストを共有しながらも、構成比が逆転 = "
        f"「法体系が異なれば施設構成も異なる」 という<b>制度的構造分極</b>を実データで確認。</p>"
        if not T_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_facil} 件 / {n_coasts} 海岸 / {n_kinds} 施設種類 / {n_jurisdictions} 所管</b> の "
      f"多次元構造。<b>護岸 {goan_share:.0f}%</b> が圧倒的主役 (H1 強支持) で、海岸法の本旨 (= 海岸線そのものを守る) を反映。"
      f"上位 3 海岸 (<b>{coast_count.index[0]} / {coast_count.index[1]} / {coast_count.index[2]}</b>) で {top3_coast_share:.0f}% (H2 {jud(top3_coast_share>=20)}) = 偏在型分布。"
      f"延長合計は約 {gdf['length_m'].sum()/1000:.0f} km、最長施設は数 km 級の長大護岸。"
      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"これは海岸法 (1956) が制定された時点で津波法 (2011) が無く、<b>55 年の制度ギャップ</b>のあいだに整備されてきた施設の主目的を反映。"
      f"<b>両方想定内 ({100*n_in_both/n_geom:.0f}%)</b> の多重ハザード地点は防御最重要、<b>どちらも外 ({100*n_in_neither/n_geom:.0f}%)</b> は侵食対策 + 港内機能等の他目的施設。</li>"
    + f"<li><b>RQ3 結論</b>: L32 港湾外郭 (防波堤 {l32_hbr_share:.0f}%) と L64 海岸保全 (護岸 {l64_goan_share:.0f}%) で "
      f"<b>構造分極が極端</b> (H4 {jud(h4_pass)})。これは法体系 (港湾法 vs 海岸法) と物理機能 (港内静穏 vs 海岸線保護) の "
      f"差異の直接的反映。地理重ね合わせでは主要港湾内で両系統が密集重複し、自然海岸では L64 単独防護。"
      f"L32 + L64 統合で {(l32_total if l32_df is not None else 842) + n_total_facil} 件 = 広島県の海岸防御網全体。"
      f"主要沿岸市町は両系統で重層的に守られ (H5 支持)、周辺小規模町は片方のみでカバー。</li>"
    + "</ul>"
    + "<h3>制度史的位置付け</h3>"
    + f"<p>本データ (#1253) は<b>「海岸法 (1956)」</b>体系の悉皆データ。"
    f"L32 (港湾法 1950 + 漁港漁場整備法) と L63 (津波防災地域づくり法 2011) と並んで、"
    f"日本の<b>沿岸 3 法体系</b> (港湾 + 海岸 + 津波) の役割分担を物理データで描き出す。"
    f"特に海岸法は<b>1953 年和歌山有田川 + 1959 年伊勢湾台風</b>の高潮被災の前後に制定され、"
    f"55 年間 (1956-2011) 日本の海岸防御の基本法だった。"
    f"その時代の整備実態が本データに刻まれている = <b>「高潮主目的整備」</b>の歴史的痕跡が "
    f"高潮カバー率 ({l44_cover_pct:.0f}%) > 津波カバー率 ({l49_cover_pct:.0f}%) として観測される。</p>"
    + f"<p>本研究の重要発見は<b>「L32 防波堤 {l32_hbr_share:.0f}% vs L64 護岸 {l64_goan_share:.0f}% の構造分極」</b>と "
    f"<b>「高潮カバー率 - 津波カバー率 = {l44_cover_pct - l49_cover_pct:+.1f} pt の制度時間差」</b>。"
    f"前者は<b>同じ沿岸で 2 法体系が役割分担している</b>事実を、"
    f"後者は<b>制度の世代差が施設整備に時系列的に刻まれる</b>事実を初めて定量化した。"
    f"これらは防災行政の<b>「沿岸防護の制度地理学」</b>と "
    f"<b>「ハザード想定の世代差と施設整備の関係」</b>という新たな研究テーマを開く。</p>"
    + "<h3>海岸法の制度進化と本データの位置付け</h3>"
    + f"<p>海岸法は<b>1956 年 5 月 12 日</b>に制定された。"
    f"これは<b>1953 年和歌山県有田川流域大水害 + 1959 年伊勢湾台風</b>を前後する時期で、"
    f"<b>高潮防御を主目的</b>に立法された。"
    f"その後<b>1999 年改正</b>で「海岸環境の整備と保全」 「海岸の適正利用」 が目的に追加され、"
    f"<b>2011 年</b>東日本大震災後の津波防災地域づくり法制定で「津波防御」 が補完された。"
    f"本データは <b>2023-05-09 最終更新</b>なので、海岸法 + 1999 年改正 + 津波法補完を経た現代の整備実態を反映。"
    f"ただし<b>過半の施設は 2011 年以前の整備</b>と推測され、これが高潮カバーが津波カバーを上回る "
    f"歴史的痕跡として観測される根拠。</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 年以前」 と推定される (= 海岸法時代)。</li>
  <li><b>新仮説 Y</b>: 整備後 30 年以上経過した護岸が大半を占め、<b>老朽化リスク</b>が高い。
      特に<b>1960-1980 年代の高度成長期</b>に集中整備された護岸は、
      塩害 + 凍結融解 + 波浪疲労で<b>耐用年数 50 年</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>: 本研究では施設種類で分類したが、<b>実際の防御能力</b> (= 設計波高・天端高) は未取得。
      同じ「護岸」 でも 1m 級と 5m 級では性能が桁違い。</li>
  <li><b>新仮説 Y</b>: 防御能力は<b>設計波高 × 天端高</b>でスコア化でき、施設種類別 + 海岸別に
      <b>「防御能力空間分布」</b>を描ける。これは老朽化スコアと組み合わせた<b>リスク管理マップ</b>の前段。</li>
  <li><b>課題 Z</b>: 広島県の維持管理データ (=本データに含まれる「維持管理情報」 は別ファイル想定) から
      天端高・設計波高を取得 → 施設別防御能力スコアを算出 →
      ハザード想定 (L44 max + L49) との<b>「能力 vs 脅威」 ギャップ</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>を抽出し、
      その境界線上で 50 m 以内に施設が無いセグメントは<b>「未防御沿岸線」</b>として
      重点整備候補になる。これは沿岸長 km 単位で定量化可能。</li>
  <li><b>課題 Z</b>: <code>l44_max.boundary</code> で polygon 境界線を取得 →
      sampling で 50m 間隔の点列を生成 → 各点から最近傍施設までの距離を
      <code>shapely.STRtree</code> で計算 → <b>未防御セグメントを地図化</b>。
      市町別に未防御沿岸長を集計し、<b>「未防御度ランキング」</b>を作成。
      <b>「防御の地理的隙間学」</b>として展開。</li>
</ul>

<h4>発展課題 4 (RQ3 由来): <b>L32 + L64 の連携検証</b> — 港湾内重複の効率性</h4>
<ul>
  <li><b>結果 X</b>: 本研究では L32 + L64 が主要港湾エリアで重複することを地理的に確認したが、
      <b>「重複は効率的か、無駄か」</b>は未評価。同じエリアを 2 系統で守ると重複投資になる可能性。</li>
  <li><b>新仮説 Y</b>: L32 と L64 の重複は<b>機能分担</b>を伴うため無駄ではない。
      L32 は港口部 (波浪遮断), L64 は港内陸地境界 (海岸線保護) で
      役割が異なる。重複でも機能差があるなら効率的、機能が同じなら重複投資。</li>
  <li><b>課題 Z</b>: 主要港湾 (広島港・福山港・尾道糸崎港) ごとに L32 + L64 を地図化 →
      機能スコア (波浪遮断 / 海岸線保護) でクラスタリング →
      <b>「機能分担 vs 重複投資」</b>を判定。重複投資が無いと判定されれば
      連携の効率性を実証。<b>「2 法体系の効率性検証」</b>として展開。</li>
</ul>

<h4>発展課題 5 (RQ3 拡張): <b>海岸保全区域 (= 法的指定エリア)</b>との照合</h4>
<ul>
  <li><b>結果 X</b>: 本データは<b>施設</b>のみで、海岸法に基づく<b>海岸保全区域</b>(= 区域指定)
      は別データ。両者の関係 (= 区域内に施設があるか) は未確認。</li>
  <li><b>新仮説 Y</b>: 海岸保全施設は<b>海岸保全区域内に限定して整備</b>される (= 法的根拠)。
      区域外の施設は皆無、区域内施設密度は区域面積に比例する。</li>
  <li><b>課題 Z</b>: DoBoX で「海岸保全区域」 のデータセット (別 dataset_id) を検索 →
      Shapefile を取得 → 本データ施設を sjoin → 区域内施設率を市町別に集計。
      <b>「区域指定 vs 施設整備」</b>の制度的整合性を検証。
      <b>「海岸法の制度地理学」</b>として展開し、L63 (津波警戒区域) との比較も可能。</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>から各施設 centroid の標高を抽出 →
      施設種類別ヒストグラム → <b>「標高分布の施設種類差」</b>を統計検定。
      胸壁の標高が他より有意に高いことを検証。
      <b>「標高 + 施設種類 + ハザード」 の 3 軸統合分析</b>として展開。</li>
</ul>

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

# 統合
sections = [
    ("学習目標と問い", sec1),
    ("使用データ", sec2),
    ("ダウンロード", sec3),
    (f"【RQ1】 海岸保全施設の構造 — 護岸 {goan_share:.0f}% / "
     f"{n_coasts} 海岸 / Top 3 で {top3_coast_share:.0f}%",
     sec4),
    (f"【RQ2】 高潮 (L44) + 津波 (L49) カバレッジ — "
     f"高潮 {l44_cover_pct:.0f}% / 津波 {l49_cover_pct:.0f}%",
     sec5),
    (f"【RQ3】 L32 港湾外郭との役割分担 — "
     f"防波堤 {l32_hbr_share:.0f}% (L32) vs 護岸 {l64_goan_share:.0f}% (L64)",
     sec6),
    ("仮説検証総合", sec7),
    ("発展課題", sec8),
]


html = render_lesson(
    num=64,
    title="海岸保全施設 単独 3 研究例分析 — "
          f"{n_total_facil} 件から県の海岸防御戦略を読む",
    tags=["L64", "海岸保全施設", "海岸法 (1956)",
          "RQ×3", "Format B", "geopandas", "WKT (CSV)",
          "choropleth", "L32連携 (港湾外郭)", "L44連携 (高潮)",
          "L49連携 (津波)", "構造分極", "陸海境界の最前線"],
    time="50 分",
    level="中級",
    data_label=f"DoBoX dataset 1253 (CSV, ~517 KB) "
               f"+ L32 既扱 + L44/L49 既キャッシュ",
    sections=sections,
    script_filename="L64_coast_protection.py",
)

OUT_HTML = LESSONS / "L64_coast_protection.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:.2f}s ===")
