Files
Aislo/E06_StabRock/E06_StabRock_UI_Result.ts
eomsangdonandClaude Opus 5.5 53c30db57b feat(E01·E04·E06·E07): 측점 글 = No.+거리 소수 한 자리 · 간격 = 측점 목록 추정 (PLAN 566-4)
- 공용 stationLabel · inferStationInterval 로 한 벌(예 61+14.5) · 서버 label(STA. 꼴) 안 읽음 · 화면만
- E07 — 측점 표 · 지도 측점 글 · 셀 툴팁 · 지도 글 솎음이 20 m 고정 대신 추정 간격
- E04 — 구간 목록 · 위치(가까운 측점) · 지도 측점 글 · 툴팁 · 솎음 간격
- E06 — 위험 띠 · 측점 표 · 상세 제목 · 스테레오넷 제목 · 띠 양 끝 글
- E01 빠진 측점 띠 — interval 인자(기본 20 m · E02 · E03 이 측점 간격을 넘기면 그 값)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:07:47 +09:00

217 lines
7.3 KiB
TypeScript
Raw Permalink 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";
import { e06Table, note, verdictLamp } from "./E06_StabRock_UI_Parts";
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 verdictLamp(verdict, 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: list[0].label ?? `${lo.toFixed(0)} m` }),
el("span", { text: list[list.length - 1].label ?? `${hi.toFixed(0)} m` }),
);
box.append(legend, strip, ticks);
} else box.append(legend, note(t("E06_NoRock")));
return box;
}
export function buildList(
stations: StationEntry[],
active: string | null,
onSelect: (id: string) => void,
): HTMLElement {
const heads = ["E06_L_Station", "E06_L_Side", "E06_L_Modes", "E06_L_MinF", "E06_L_Verdict"];
const list = e06Table(
heads.map((key) => t(key as LocaleKey)),
stations.map((e) => [
e.label ?? e.key,
t(e.slope.side === "left" ? "E06_Left" : "E06_Right"),
`${e.counts.planar} · ${e.counts.wedge} · ${e.counts.toppling}`,
fmt(e.min_F),
badge(e.verdict),
]),
"e06-list",
"station-list",
);
list.rows.forEach((tr, i) => {
const e = stations[i];
const id = entryId(e);
tr.dataset.id = id;
tr.dataset.verdict = e.verdict;
tr.classList.toggle("is-active", id === active);
tr.addEventListener("click", () => onSelect(id));
});
return list.root;
}
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")}` }),
note(
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(note(kinText.join("\n"), "e06-kin"));
if (!e.cases.length) {
box.append(note(t(e.joints.length ? "E06_NoRisk" : "E06_NoJointsHere")));
return box;
}
const heads = [
"E06_C_Mode",
"E06_C_Joints",
"E06_C_Cond",
"E06_C_F",
"E06_C_Req",
"E06_L_Verdict",
];
const cases = e06Table(
heads.map((key) => t(key as LocaleKey)),
e.cases.map((c) => {
const verdict: Verdict = c.F === null ? "check" : c.ok === false ? "NG" : "OK";
const text = !c.formed
? t("E06_NotFormed")
: c.small
? t("E06_Small")
: c.persistence_short
? t("E06_Persist")
: "";
return [
t(`E06_Mode_${c.mode}` as LocaleKey),
c.ids.join("·"),
t(`E06_Cond_${c.condition}` as LocaleKey),
`${fmt(c.F, 3)}${text ? ` ${text}` : ""}`,
fmt(c.required),
c.small ? "–" : badge(verdict),
];
}),
"e06-cases",
);
cases.rows.forEach((tr, i) => {
tr.dataset.mode = e.cases[i].mode;
tr.dataset.cond = e.cases[i].condition;
});
box.append(cases.root);
if (e.small_blocks) box.append(note(t("E06_SmallCount").replace("{n}", String(e.small_blocks))));
if (kin.toppling.length) box.append(note(t("E06_ToppleNote")));
return box;
}