Files
Aislo/E06_StabRock/E06_StabRock_UI_Result.ts
T

236 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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<Record<Verdict, number>>;
modes: Record<string, number>;
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<number, StationEntry>();
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;
}