236 lines
8.1 KiB
TypeScript
236 lines
8.1 KiB
TypeScript
/* =============================================================================
|
||
* 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;
|
||
}
|