- common_util_mass_haul_balance 842 → 467 (_types · _apply 분리 · 원래 파일에서 타입 다시 내보냄) - B05_Profile_UI_Profile_Table 702 → 548 (_Table_Selected 분리) - test_507_3 는 옮긴 글을 새 파일에서 찾음 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
174 lines
8.4 KiB
TypeScript
174 lines
8.4 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Profile_Table_Selected.ts
|
|
* 종단면도 하단 표 — 고른 측점의 **값 열 오버레이**(`buildSelectedColumn`)와 표 본체가 같이 쓰는 조각
|
|
* (`element` · `displayStationLabel` · `isStraightCurve`). 표 본체는 `B05_Profile_UI_Profile_Table`.
|
|
* ========================================================================== */
|
|
|
|
import type {
|
|
AlignmentCurve,
|
|
AlignmentSegment,
|
|
ProfileAlignment,
|
|
} from "./logic/B05_Profile_UI_Profile_Alignment";
|
|
import { stationLabel } from "../B06_Section/B06_Section_UI_Section_Common";
|
|
|
|
/**
|
|
* 대수차가 이 값보다 작으면 **직선**으로 본다(%).
|
|
*
|
|
* 직선화하면 그 자리의 좌·우 기울기가 같아져 대수차가 0 이 되고, `L = R × |대수차|` 를
|
|
* 뒤집은 R 은 수억 m 로 튄다. 직선 구간에는 반경이 없으므로 표에는 빈 칸으로 둔다
|
|
* (2026-09-04 사용자 지시). 진짜 완만한 변화점(대수차 0.9% 수준)은 그대로 남는다.
|
|
*/
|
|
const STRAIGHT_DELTA_PCT = 1e-4;
|
|
|
|
/** 직선화로 대수차가 사라진 자리인가 — 표기 대상에서 뺀다. */
|
|
export function isStraightCurve(curve: AlignmentCurve): boolean {
|
|
return Math.abs(curve.delta_pct) < STRAIGHT_DELTA_PCT;
|
|
}
|
|
|
|
export function element(tag: string, className: string, text?: string): HTMLElement {
|
|
const node = document.createElement(tag);
|
|
node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
/** 이어 공사 시작 기준을 반영한 측점 표기 — 측점번호에 시작 측점을 더한다(잔여거리는 그대로). */
|
|
export function displayStationLabel(
|
|
chainageM: number,
|
|
interval: number,
|
|
stationOffset: number,
|
|
): string {
|
|
const base = stationLabel(chainageM, interval); // "X+YY.Y"
|
|
if (!stationOffset) return base;
|
|
const [number, remainder] = base.split("+");
|
|
return `${Number(number) + stationOffset}+${remainder}`;
|
|
}
|
|
|
|
/** 정렬된 계획선 샘플에서 chainage 위치 값을 선형보간한다(범위 밖은 양 끝 클램프). */
|
|
function interpolateSample(
|
|
samples: ProfileAlignment["samples"],
|
|
chainageM: number,
|
|
key: "elevation_m" | "ground_elevation_m",
|
|
): number | null {
|
|
if (!samples.length) return null;
|
|
if (chainageM <= samples[0].chainage_m) return samples[0][key];
|
|
const last = samples[samples.length - 1];
|
|
if (chainageM >= last.chainage_m) return last[key];
|
|
for (let index = 1; index < samples.length; index += 1) {
|
|
const current = samples[index];
|
|
if (current.chainage_m < chainageM) continue;
|
|
const previous = samples[index - 1];
|
|
const span = current.chainage_m - previous.chainage_m;
|
|
if (span <= 1e-9) return current[key];
|
|
const ratio = (chainageM - previous.chainage_m) / span;
|
|
return previous[key] + (current[key] - previous[key]) * ratio;
|
|
}
|
|
return last[key];
|
|
}
|
|
|
|
/** 값 열 한 칸의 내용: 단일 값이거나, 변화점에서 좌/우로 갈린 두 값. */
|
|
type CellValue = { text: string } | { left: string; right: string };
|
|
|
|
/**
|
|
* 선택된 측점(규칙·비정규 공용)의 **값 열 오버레이**. 12행 순서 그대로 값(계획고·지반고·누가거리·
|
|
* 측점·구배·곡선 등)을 쌓아 테이블 위에 겹친다. 규칙 측점을 눌러도 비정규와 **같은 오버레이 방식**으로
|
|
* 눈에 잘 들어오게 한다. `centerX`는 호출부가 정한다 — 종점처럼 셀이 우측 이동된 규칙 측점은 그 이동된
|
|
* 중심을, 비정규 측점은 `x(chainage)`를 넘긴다.
|
|
*/
|
|
export function buildSelectedColumn(
|
|
chainage: number,
|
|
alignment: ProfileAlignment,
|
|
centerX: number,
|
|
cellWidth: number,
|
|
interval: number,
|
|
display: { station: number; cumulative: number },
|
|
onAdjustStation?: (chainageM: number, deltaM: number) => void,
|
|
isIrregular = false,
|
|
mc = false,
|
|
): HTMLElement {
|
|
const plan = interpolateSample(alignment.samples, chainage, "elevation_m");
|
|
const ground = interpolateSample(alignment.samples, chainage, "ground_elevation_m");
|
|
const cut = plan !== null && ground !== null && ground - plan > 0.005 ? ground - plan : null;
|
|
const fill = plan !== null && ground !== null && plan - ground > 0.005 ? plan - ground : null;
|
|
|
|
// 구배(L/H/S): 이 측점이 구간 안에 있으면 그 구간 값을 그대로(중복이어도) 표기하고,
|
|
// 변화점이라 좌우 구간이 갈리면 세로 구분선으로 셀을 반 나눠 좌/우 값을 각각 넣는다.
|
|
const inside = alignment.segments.find(
|
|
(segment) => segment.from_m + 1e-6 < chainage && chainage < segment.to_m - 1e-6,
|
|
);
|
|
const leftSeg = alignment.segments.find((segment) => Math.abs(segment.to_m - chainage) < 0.01);
|
|
const rightSeg = alignment.segments.find((segment) => Math.abs(segment.from_m - chainage) < 0.01);
|
|
const gradeCell = (pick: (segment: AlignmentSegment) => number, digits: number): CellValue => {
|
|
if (inside) return { text: pick(inside).toFixed(digits) };
|
|
const left = leftSeg ? pick(leftSeg).toFixed(digits) : "";
|
|
const right = rightSeg ? pick(rightSeg).toFixed(digits) : "";
|
|
return left && right ? { left, right } : { text: left || right || "" };
|
|
};
|
|
const curveAt = alignment.curves.find((entry) => Math.abs(entry.chainage_m - chainage) < 0.01);
|
|
// 값 열 오버레이도 같은 규칙 — 직선화된 자리는 곡선 L·R 을 비운다.
|
|
const curve = curveAt && !isStraightCurve(curveAt) ? curveAt : undefined;
|
|
// 테이블 행 순서(구배 1 · 측점값 3 · 곡선 2)와 정확히 같게 채운다. 없는 값은 공백.
|
|
// `plan: true`인 계획고 행만 직접 입력 셀로 만든다(값 열은 오버레이라 실제 6행 격자는 불변).
|
|
// 절토·성토고는 행이 빠졌어도 값 자체는 툴팁에 남긴다 — 고른 측점에서 흔히 찾는 값이다.
|
|
const rows: Array<{ value: CellValue; modifier: string; plan?: boolean }> = [
|
|
{ value: gradeCell((segment) => segment.grade_percent, 2), modifier: "grade" }, // 구배 S
|
|
{ value: { text: plan !== null ? plan.toFixed(2) : "" }, modifier: "plan", plan: true }, // 계획고
|
|
{ value: { text: ground !== null ? ground.toFixed(2) : "" }, modifier: "" }, // 지반고
|
|
{
|
|
value: {
|
|
text: displayStationLabel(chainage, interval, display.station) + (mc ? " (MC)" : ""),
|
|
},
|
|
modifier: mc ? "mc" : "",
|
|
}, // 측점
|
|
{ value: { text: curve ? curve.l_m.toFixed(2) : "" }, modifier: "" }, // 곡선 L
|
|
{ value: { text: curve ? curve.r_m.toFixed(1) : "" }, modifier: "" }, // 곡선 R
|
|
];
|
|
const column = element(
|
|
"div",
|
|
`b05-profile-table__irregular-col${isIrregular ? " is-floating" : ""}`,
|
|
);
|
|
column.style.left = `${centerX}px`;
|
|
column.style.width = `${cellWidth}px`;
|
|
rows.forEach((row) => {
|
|
const cell = element(
|
|
"div",
|
|
`b05-profile-table__irregular-col-cell${row.modifier ? ` is-${row.modifier}` : ""}`,
|
|
);
|
|
if (row.plan && onAdjustStation && plan !== null) {
|
|
// 계획고 직접 입력 → (입력값 − 현재 계획고) delta로 station_offset 반영(규칙·비정규 공용).
|
|
const input = document.createElement("input");
|
|
input.type = "number";
|
|
input.step = "any";
|
|
input.className = "b05-profile-table__irregular-input b05-profile-table__no-spin";
|
|
input.value = plan.toFixed(2);
|
|
input.title = `계획고 직접 입력 (현재 ${plan.toFixed(2)}m)`;
|
|
input.addEventListener("change", () => {
|
|
const target = Number.parseFloat(input.value);
|
|
if (Number.isFinite(target)) onAdjustStation(chainage, target - plan);
|
|
});
|
|
cell.append(input);
|
|
} else if ("left" in row.value) {
|
|
// 좌우 값을 세로 구분선으로 반씩 나눠 넣는다(각 반쪽에서 폰트를 줄여 맞춘다).
|
|
cell.classList.add("is-split");
|
|
cell.append(
|
|
element("span", "b05-profile-table__irregular-col-half", row.value.left),
|
|
element("span", "b05-profile-table__irregular-col-half", row.value.right),
|
|
);
|
|
} else {
|
|
cell.textContent = row.value.text;
|
|
}
|
|
column.append(cell);
|
|
});
|
|
// 행에서 뺀 값(절토고·성토고·누가거리)은 툴팁에 남긴다 — 고른 측점에서 흔히 찾는 값이다.
|
|
const cutFillText =
|
|
cut !== null
|
|
? ` · 절토 ${cut.toFixed(2)}m`
|
|
: fill !== null
|
|
? ` · 성토 ${fill.toFixed(2)}m`
|
|
: "";
|
|
column.title = `${displayStationLabel(chainage, interval, display.station)} · 누가거리 ${(
|
|
chainage + display.cumulative
|
|
).toFixed(2)}m${cutFillText}`;
|
|
return column;
|
|
}
|