/* ============================================================================= * E06_StabRock_UI_Result.ts * E06 결과 — 위험 띠(누가 축 · 측점 판정 색 · 누르면 그 측점) · 우측 측점 목록(쪽 · 모드 수 · * 최소 안전율 · 판정) · 고른 측점 사면 · 모드별 안전율 표. 값은 서버 저장본 그대로(계산 없음). * ========================================================================== */ import { el } from "@ui/ui_template_elements"; import { t, type LocaleKey } from "@ui/ui_template_locale"; export type Verdict = "NG" | "check" | "OK" | "none"; export interface RockCase { mode: "planar" | "wedge"; ids: string[]; condition: "dry" | "wet" | "seismic"; F: number | null; formed: boolean; persistence_short: boolean; small: boolean; required: number | null; ok: boolean | null; sliding?: string | null; } export interface StationEntry { key: string; chainage_m: number; label: string | null; ground_type: string; slope: { side: string; dip_direction: number; dip: number; height_m: number; upper_dip: number }; gamma_r: number; joints: { id: string; name: string; dip_direction: number; dip: number; phi: number; c: number; }[]; kinematic: { planar: string[]; toppling: string[]; wedge: { ids: [string, string]; trend: number; plunge: number }[]; }; cases: RockCase[]; counts: { planar: number; wedge: number; toppling: number }; min_F: number | null; small_blocks: number; verdict: Verdict; } export interface RockResult { fingerprint: string; summary: { slopes: number; verdicts: Partial>; modes: Record; small_blocks: number; }; stations: StationEntry[]; } const ORDER: Verdict[] = ["NG", "check", "OK", "none"]; export const entryId = (e: StationEntry): string => `${e.key}|${e.slope.side}`; const fmt = (v: number | null | undefined, d = 2): string => v === null || v === undefined ? "–" : v.toFixed(d); function badge(verdict: Verdict): HTMLElement { return el("span", { className: `e06-verdict e06-verdict--${verdict.toLowerCase()}`, text: t(`E06_V_${verdict}` as LocaleKey), }); } /** 위험 띠 — 측점마다 가장 나쁜 판정 · 이웃 측점 가운데서 끊음 */ export function buildBand( stations: StationEntry[], active: string | null, onSelect: (id: string) => void, ): HTMLElement { const box = el("div", { className: "e06-band", attrs: { "data-role": "band" } }); const worst = new Map(); for (const e of stations) { const prev = worst.get(e.chainage_m); if (!prev || ORDER.indexOf(e.verdict) < ORDER.indexOf(prev.verdict)) worst.set(e.chainage_m, e); } const list = [...worst.values()].sort((a, b) => a.chainage_m - b.chainage_m); const legend = el("div", { className: "e06-band__legend" }); legend.append(el("strong", { text: t("E06_Band") })); for (const v of ORDER) { const n = list.filter((e) => e.verdict === v).length; if (n) legend.append(badge(v), el("span", { className: "e06-band__count", text: String(n) })); } const strip = el("div", { className: "e06-band__strip" }); if (list.length) { const lo = list[0].chainage_m; const hi = Math.max(list[list.length - 1].chainage_m, lo + 1); list.forEach((e, i) => { const a = i ? (list[i - 1].chainage_m + e.chainage_m) / 2 : lo; const b = i < list.length - 1 ? (e.chainage_m + list[i + 1].chainage_m) / 2 : hi; const seg = el("button", { className: `e06-band__seg e06-band__seg--${e.verdict.toLowerCase()}${ active?.startsWith(`${e.key}|`) ? " is-active" : "" }`, attrs: { type: "button", "data-key": e.key, title: `${e.label ?? e.key} · ${t(`E06_V_${e.verdict}` as LocaleKey)}`, }, }); seg.style.left = `${((a - lo) / (hi - lo)) * 100}%`; seg.style.width = `${Math.max(0.4, ((b - a) / (hi - lo)) * 100)}%`; seg.addEventListener("click", () => onSelect(entryId(e))); strip.append(seg); }); const ticks = el("div", { className: "e06-band__ticks" }); ticks.append( el("span", { text: `${lo.toFixed(0)} m` }), el("span", { text: `${hi.toFixed(0)} m` }), ); box.append(legend, strip, ticks); } else box.append(legend, el("p", { className: "e06-hint", text: t("E06_NoRock") })); return box; } export function buildList( stations: StationEntry[], active: string | null, onSelect: (id: string) => void, ): HTMLElement { const table = el("table", { className: "e06-list", attrs: { "data-role": "station-list" } }); const head = el("tr"); for (const key of [ "E06_L_Station", "E06_L_Side", "E06_L_Modes", "E06_L_MinF", "E06_L_Verdict", ] as LocaleKey[]) head.append(el("th", { text: t(key) })); table.append(el("thead", { children: [head] }), el("tbody")); for (const e of stations) { const id = entryId(e); const tr = el("tr", { className: id === active ? "is-active" : "", attrs: { "data-id": id, "data-verdict": e.verdict }, }); tr.append( el("td", { text: e.label ?? e.key }), el("td", { text: t(e.slope.side === "left" ? "E06_Left" : "E06_Right") }), el("td", { text: `${e.counts.planar} · ${e.counts.wedge} · ${e.counts.toppling}` }), el("td", { text: fmt(e.min_F) }), el("td", { children: [badge(e.verdict)] }), ); tr.addEventListener("click", () => onSelect(id)); table.tBodies[0].append(tr); } return table; } export function buildDetail(e: StationEntry): HTMLElement { const box = el("div", { className: "e06-detail", attrs: { "data-role": "detail" } }); const s = e.slope; box.append( el("h4", { text: `${e.label ?? e.key} · ${t(s.side === "left" ? "E06_Left" : "E06_Right")}` }), el("p", { className: "e06-hint", text: t("E06_SlopeLine") .replace("{dd}", s.dip_direction.toFixed(0)) .replace("{dip}", s.dip.toFixed(1)) .replace("{h}", s.height_m.toFixed(2)) .replace("{up}", s.upper_dip.toFixed(1)) .replace("{g}", e.gamma_r.toFixed(1)), }), ); const kin = e.kinematic; const kinText = [ `${t("E06_Mode_planar")} ${kin.planar.join(", ") || "–"}`, `${t("E06_Mode_wedge")} ${kin.wedge.map((w) => `${w.ids.join("·")}(${w.trend.toFixed(0)}/${w.plunge.toFixed(0)})`).join(", ") || "–"}`, `${t("E06_Mode_toppling")} ${kin.toppling.join(", ") || "–"}`, ]; box.append(el("p", { className: "e06-kin", text: kinText.join("\n") })); if (!e.cases.length) { box.append( el("p", { className: "e06-hint", text: t(e.joints.length ? "E06_NoRisk" : "E06_NoJointsHere"), }), ); return box; } const table = el("table", { className: "e06-cases" }); const head = el("tr"); for (const key of [ "E06_C_Mode", "E06_C_Joints", "E06_C_Cond", "E06_C_F", "E06_C_Req", "E06_L_Verdict", ] as LocaleKey[]) head.append(el("th", { text: t(key) })); table.append(el("thead", { children: [head] }), el("tbody")); for (const c of e.cases) { const verdict: Verdict = c.F === null ? "check" : c.ok === false ? "NG" : "OK"; const note = !c.formed ? t("E06_NotFormed") : c.small ? t("E06_Small") : c.persistence_short ? t("E06_Persist") : ""; const tr = el("tr", { attrs: { "data-mode": c.mode, "data-cond": c.condition } }); tr.append( el("td", { text: t(`E06_Mode_${c.mode}` as LocaleKey) }), el("td", { text: c.ids.join("·") }), el("td", { text: t(`E06_Cond_${c.condition}` as LocaleKey) }), el("td", { text: `${fmt(c.F, 3)}${note ? ` ${note}` : ""}` }), el("td", { text: fmt(c.required) }), el("td", { children: [c.small ? el("span", { text: "–" }) : badge(verdict)] }), ); table.tBodies[0].append(tr); } box.append(table); if (e.small_blocks) box.append( el("p", { className: "e06-hint", text: t("E06_SmallCount").replace("{n}", String(e.small_blocks)), }), ); if (kin.toppling.length) box.append(el("p", { className: "e06-hint", text: t("E06_ToppleNote") })); return box; }