/* ============================================================================= * E03_StabSection_UI_Plan.ts * E03 위 평면 위험 띠(PLAN 532-3) — 측점 중심을 이은 노선을 그 조건 판정 색으로(이웃 측점 사이 * 가운데서 끊음 · 북쪽 위 · 같은 축척) · 고른 측점 표시 · 누르면 가장 가까운 측점. * 범례 = 판정마다 아이콘 + 글 + 측점 수. 계산은 logic `bandSegments` · `planFit`. * ========================================================================== */ import { el } from "@ui/ui_template_elements"; import { t, type LocaleKey } from "@ui/ui_template_locale"; import { bandSegments, planFit, verdictOf, type Condition, type Point, type StationRow, } from "./logic/E03_StabSection_Logic"; import { svgEl } from "./E03_StabSection_UI_Section"; const H = 96; const PAD = 10; export function buildPlan( rows: StationRow[], condition: Condition, active: string | null, onSelect: (key: string) => void, ): HTMLElement { const box = el("div", { className: "e03-plan e03-card" }); const head = el("div", { className: "e03-card__head" }); head.append(el("h3", { className: "e03-card__title", text: t("E03_Plan") })); const legend = el("div", { className: "e03-legend", attrs: { "data-role": "plan-legend" } }); const counts = { ok: 0, ng: 0, none: 0 }; rows.forEach((r) => (counts[verdictOf(r, condition)] += 1)); for (const v of ["ng", "ok", "none"] as const) { if (!counts[v]) continue; const item = el("span", { className: `e03-verdict e03-verdict--${v}` }); item.append(`${t(`E03_Verdict_${v}` as LocaleKey)} ${counts[v]}`); legend.append(item); } head.append(legend); box.append(head); const host = el("div"); box.append(host); const draw = (): void => { const W = Math.max(host.clientWidth, 300); const segs = bandSegments(rows, condition); const all: Point[] = segs.flatMap((s) => s.points); const svg = svgEl("svg", { viewBox: `0 0 ${W} ${H}`, "data-role": "plan" }); if (all.length) { const fit = planFit(all, W, H, PAD); const scr = (p: Point): Point => [fit.k * p[0] + fit.tx, -fit.k * p[1] + fit.ty]; for (const seg of segs) { svg.append( svgEl("polyline", { points: seg.points.map((p) => scr(p).join(",")).join(" "), class: `e03-plan__seg e03-plan__seg--${seg.verdict}`, "data-verdict": seg.verdict, }), ); } const centers = rows.filter((r) => r.center && r.center[0] !== null && r.center[1] !== null); const picked = centers.find((r) => r.key === active); if (picked) { const [x, y] = scr([picked.center![0]!, picked.center![1]!]); svg.append( svgEl("circle", { cx: x, cy: y, r: 6, class: "e03-plan__pick", "data-key": picked.key }), ); } svg.addEventListener("click", (ev) => { const box2 = svg.getBoundingClientRect(); const px = ((ev.clientX - box2.left) / box2.width) * W; const py = ((ev.clientY - box2.top) / box2.height) * H; let best: StationRow | null = null; let dist = Infinity; for (const r of centers) { const [x, y] = scr([r.center![0]!, r.center![1]!]); const d = Math.hypot(x - px, y - py); if (d < dist) [best, dist] = [r, d]; } if (best && dist < 30) onSelect(best.key); }); } host.replaceChildren(svg); }; new ResizeObserver(draw).observe(host); return box; }