2026-09-06 사용자 확정. 자동탐색은 평면만 보고 노선을 정해 실제 판단과 맞지 않으므로 쓰지 않는다 — 버튼만 감추고 코드·API 는 남긴다. 대신 법정 평면 기준을 **경고**로 낸다. - 횡단 생성이 측점마다 평면 곡선반경(`plan_radius_m`)을 낸다. 노선 폴리라인 위에서 앞뒤 10m 떨어진 세 점의 외접원 반경이며, 직선(1만m 이상)은 null. 반지름 50m 원호로 50.008 이 나오는 것을 확인. - 법정 최소곡선반지름 표(별표2 Ⅰ.2.다.(1) — 40:60/40, 30:30/20, 20:15/12)와 배향곡선 하한 10m 를 설정에 넣고 계획선 정책에 실어 화면으로 내린다. 탐색이 쓰는 등급별 상수와는 별개다. - 종단 상단줄에 「곡선반경 부족 n곳 / 최소 R」 경고 추가 — 자동 보정·차단은 없다. 툴팁에 하한값과 배향곡선 하한 미만 개수를 적는다. - 이 반경은 다음 작업(곡선부 확폭)이 그대로 쓴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
159 lines
8.7 KiB
TypeScript
159 lines
8.7 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Profile_Balance.ts
|
|
* 종단 패널 상단 표시줄 — **법정 판정에 쓰이는 값만** 남긴다(2026-08-19 재편).
|
|
*
|
|
* 표시 항목(별표2 근거):
|
|
* 최대 기울기 x.x% (상한 y%) — Ⅰ.2.라: 설계속도·지형별 상한 준수 판정
|
|
* 곡선 필요 n곳 — Ⅰ.2.마: 대수차 5% 초과 변화점의 종단곡선 삽입
|
|
* 절·성토 균형은 **하단 유토곡선**이 횡단 기준 부피(㎥)로 보여 준다 — 여기 있던 종단 기준
|
|
* 불균형(㎡ 비율)은 실제 토량이 아니라 의미가 없어 뺐다(2026-09-03). 변화점·종단곡선 개수와
|
|
* 기본 곡선길이 L은 판단 기준이 없어 뺐다(2026-08-19 사용자 지적 + 지식DB 대조).
|
|
* [저장선 복원]은 상단에서 내려 그래프 조작부로 옮겼다.
|
|
*
|
|
* 패널 본체(B05_Profile_UI_Profile_Panel)가 700줄 한계에 닿아 분리했다.
|
|
* 표시줄은 상태를 갖지 않는다 — 그릴 때마다 현재 선형·편집 상태를 인자로 받는다.
|
|
* ========================================================================== */
|
|
|
|
import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment";
|
|
import type { MinCoverViolation } from "./B05_Profile_UI_Profile_MinCover";
|
|
import type { MassHaulSummaryValues } from "./B05_Profile_UI_Profile_MassHaul";
|
|
|
|
/** 누가토량 표기 — 천 단위 구분 + 소수점 1자리(유토곡선 요약과 같은 규칙). */
|
|
function volume(value: number): string {
|
|
return `${value.toLocaleString("ko-KR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })}㎥`;
|
|
}
|
|
|
|
export interface BalanceBarParams {
|
|
/** 표시줄 컨테이너. 그릴 때마다 통째로 갈아 끼운다. */
|
|
balanceBar: HTMLElement;
|
|
/** 현재 계획선(없으면 안내만 띄운다). */
|
|
alignment: ProfileAlignment | null;
|
|
/** 계획선이 없고 저장분이 구버전 형식일 때 재계산을 안내한다. */
|
|
legacyAlignment: boolean;
|
|
/** 편집이 있었는지(초기선 복원 버튼 노출 조건). */
|
|
edited: boolean;
|
|
/** 비정규 측점이 있는지(초기선 복원 버튼 노출 조건). */
|
|
hasIrregularStations: boolean;
|
|
/** 저장되지 않은 편집이 있는지. */
|
|
dirty: boolean;
|
|
/** 요약줄 맨 앞에 놓는 도구줄(되돌리기·직선화·쉬프트·틸팅) — 2026-09-02. */
|
|
tools?: HTMLElement;
|
|
/** 횡단배수 최소 계획고 위반(2026-08-23) — 배수관·BOX암거 토피 미확보 경고. */
|
|
minCoverViolations?: MinCoverViolation[];
|
|
/** 요약줄 **오른쪽 끝**에 붙이는 묶음(줌·Y레인지 조작구) — 2026-09-04. */
|
|
trailing?: HTMLElement;
|
|
/** 유토곡선 총괄값 — 「최대 기울기」 오른쪽에 적는다(2026-09-06 사용자 지시).
|
|
* 유토곡선 패널을 접어도 이 값은 남아야 계획선을 만지며 볼 수 있다. */
|
|
massHaul?: MassHaulSummaryValues | null;
|
|
/** 법정 평면 최소곡선반지름 위반(2026-09-06) — 자동 보정 없이 **경고만** 낸다. */
|
|
planCurve?: { count: number; worstRadiusM: number; limitM: number; hairpin: number } | null;
|
|
/** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */
|
|
onResetAll: () => void;
|
|
}
|
|
|
|
export function renderBalanceBar(params: BalanceBarParams): void {
|
|
params.balanceBar.replaceChildren();
|
|
// 도구줄은 계획선이 없을 때도 둔다 — 되돌리기는 계획선 밖 조작(구조물 등)도 되돌린다.
|
|
if (params.tools) params.balanceBar.append(params.tools);
|
|
if (!params.alignment) {
|
|
if (params.legacyAlignment) {
|
|
const note = document.createElement("span");
|
|
note.className = "b05-route-profile__balance-warning";
|
|
note.textContent =
|
|
"⚠ 계획선 데이터가 구버전 형식입니다 — [최적 경로 계산]을 다시 실행하세요.";
|
|
params.balanceBar.append(note);
|
|
}
|
|
if (params.trailing) params.balanceBar.append(params.trailing);
|
|
return;
|
|
}
|
|
const { alignment } = params;
|
|
const { policy, violations } = alignment;
|
|
// 최대 종단기울기 — 법정 상한 대비가 이 화면의 첫 판정 항목이다(별표2 Ⅰ.2.라).
|
|
const steepest = alignment.segments.reduce(
|
|
(worst, segment) => Math.max(worst, Math.abs(segment.grade_percent)),
|
|
0,
|
|
);
|
|
// 종단곡선이 필요한데 빠진 변화점 — 생략은 비포장 & 대수차 5% 이하만 허용된다
|
|
// (별표2 Ⅰ.2.마 / 지식DB 종단선형 §3). skip_allowed가 그 판정 결과다.
|
|
const curvesNeeded = alignment.curves.filter(
|
|
(curve) => curve.omitted && !curve.skip_allowed,
|
|
).length;
|
|
const entries: Array<[string, string, string?]> = [
|
|
[
|
|
"최대 기울기",
|
|
`${steepest.toFixed(1)} % / 상한 ${policy.max_grade_pct.toFixed(1)} %`,
|
|
violations.length ? "over" : undefined,
|
|
],
|
|
];
|
|
// 절·성토 불균형은 2026-09-03 사용자 지시로 뺐다. 이 값은 **종단 기준**이었다 —
|
|
// 계획선과 지반선 사이 세로 면적(㎡)의 절·성토 비이지 실제 토량이 아니다. 의미 있는
|
|
// 균형은 횡단 단면적을 쌓아 부피로 내는 하단 유토곡선 요약(㎥)이고, 그쪽이 이미 있다.
|
|
// 유토곡선 총괄값 — 「최대 기울기」 **바로 오른쪽**(2026-09-06 사용자 지시). 곡선을 접어도
|
|
// 남는다. 세부 내역(절토·성토·잉여/부족)은 이 항목의 툴팁으로 내렸다 — 예전 요약 막대가
|
|
// 세부를 툴팁에 두던 방식 그대로다.
|
|
const mass = params.massHaul;
|
|
if (mass) {
|
|
entries.push(["누가토량", volume(mass.finalM3), mass.finalM3 < 0 ? "over" : undefined]);
|
|
}
|
|
// 평면 곡선반경 — 법정 하한을 밑도는 측점이 있을 때만 적는다(별표2 Ⅰ.2.다.(1)).
|
|
// 프로그램은 값만 드러내고 고치지 않는다: 노선을 바꿀지는 사용자 판단이다.
|
|
const plan = params.planCurve;
|
|
if (plan && plan.count > 0) {
|
|
entries.push([
|
|
"곡선반경 부족",
|
|
`${plan.count} 곳 / 최소 ${plan.worstRadiusM.toFixed(1)} m`,
|
|
"over",
|
|
]);
|
|
}
|
|
// 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다.
|
|
if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded} 곳`, "over"]);
|
|
// 횡단배수 최소고 표시는 2026-09-02 사용자 지시로 삭제했다. 편집 차단(강제)은
|
|
// `B05_Profile_UI_Profile_Render.ts` 에 그대로 남아 있고 기본 해제다.
|
|
const editedCount = Object.keys(alignment.edits.station_offsets).length;
|
|
if (editedCount) entries.push(["편집 측점", `${editedCount} 개`, "edited"]);
|
|
entries.forEach(([label, value, tone]) => {
|
|
const item = document.createElement("span");
|
|
item.className = `b05-route-profile__balance-item${tone ? ` is-${tone}` : ""}`;
|
|
const caption = document.createElement("em");
|
|
caption.textContent = label;
|
|
item.append(caption, document.createTextNode(value));
|
|
// 상한 초과 구간의 내역은 별도 경고 칩 대신 이 항목의 툴팁으로 붙인다 —
|
|
// 같은 사실을 두 번 적지 않는다(2026-08-19 재편).
|
|
if (label === "곡선반경 부족" && plan) {
|
|
item.title = [
|
|
`법정 하한 ${plan.limitM.toFixed(1)}m 미만인 측점 ${plan.count}곳`,
|
|
`가장 급한 곳 R=${plan.worstRadiusM.toFixed(1)}m`,
|
|
plan.hairpin > 0
|
|
? `그중 배향곡선 하한 미만 ${plan.hairpin}곳 — 경고만 낸다(자동 보정 없음)`
|
|
: "배향곡선 하한 미만은 없음",
|
|
].join("\n");
|
|
}
|
|
if (label === "누가토량" && mass) {
|
|
item.title = [
|
|
`절토(자연) ${volume(mass.cutNaturalM3)} · 절토(다짐) ${volume(mass.cutCompactedM3)}`,
|
|
`성토(다짐) ${volume(mass.fillCompactedM3)}`,
|
|
mass.shortageM3 > 0 ? `부족 ${volume(mass.shortageM3)}` : `잉여 ${volume(mass.surplusM3)}`,
|
|
].join("\n");
|
|
}
|
|
if (label === "최대 기울기" && violations.length) {
|
|
item.title = violations
|
|
.map(
|
|
(entry) =>
|
|
`구간 ${entry.segment_index + 1}: ${entry.value.toFixed(2)}% > ${entry.limit}%`,
|
|
)
|
|
.join("\n");
|
|
}
|
|
params.balanceBar.append(item);
|
|
});
|
|
// [편집 되돌리기] 버튼은 2026-09-02 사용자 지시로 삭제했다 — 한 단계씩 되돌리는
|
|
// undo/redo가 대신하며, 최초 자동 계산 상태 복귀는 좌측 하단 [초기화]가 맡는다.
|
|
if (params.dirty) {
|
|
const badge = document.createElement("span");
|
|
badge.className = "b05-route-profile__balance-item is-unsaved";
|
|
badge.textContent = "미저장 (확정 시 반영)";
|
|
params.balanceBar.append(badge);
|
|
}
|
|
// 줌 조작구는 늘 오른쪽 끝 — 그래프 우측 상단 자리다(2026-09-04 사용자 지시).
|
|
if (params.trailing) params.balanceBar.append(params.trailing);
|
|
}
|