Files
Aislo/B06_Section/logic/B06_Section_Cross_Save.ts
T

338 lines
14 KiB
TypeScript

/* =============================================================================
* B06_Section_Cross_Save.ts
* [저장] · [확정] 서버 횡단 한 바퀴(PLAN 511-B B-4 · 서버 Node `cross.enforce`) — 저장 입력으로 모든 측점 설계선
* 다시 냄 → 미교차 넓힘(DTM 격자) → 포장 구간 → 세월교 노면 하강. 차례 · 승계 · 입력 = 파이썬 그대로.
*
* ⚠ ① 짝 — `B06_Section_Router_Design.py` `enforce_design_lines` · `enforce_pavement_ranges` ·
* `enforce_ford_surface_drops` + `B06_Section_Engine_SampleExtend.py` `extend_unclosed_sections`
* (서버 `_enforce_stored_designs` 차례 · DTM 아닌 지표면은 파이썬 길) · 저장분 입력 읽기
* (`stored_cut_slope` · `berm_from` · `stored_shoulder_extend` · `stored_tilt` · `curve_widening_args`) ·
* 계획고 = `design_elevation_from_longitudinal`. 거울 시험 `test_511_b4_cross_save_mirror.py`.
* crossOptions = 파이썬 `compute_cross_design` 키 → TS 짝 키(Node `cross.design` 도 이것).
* 순수 — 화면 · fetch · 세션 없음.
* ========================================================================== */
import { bermSpansFromStructures, bermSpecAt } from "@util/common_util_berm_spans";
import {
BERM_DEFAULT_DEPTH_M,
BERM_DEFAULT_INTERVAL_M,
BERM_DEFAULT_SLOPE_DEG,
BERM_DEFAULT_WIDTH_M,
type BermSpec,
} from "@util/common_util_cross_berm";
import { computeCrossDesign } from "@util/common_util_cross_design";
import type { CrossDesignOptions, CrossGroundSample } from "@util/common_util_cross_design";
import type { CulvertPipePoint } from "@util/common_util_culvert_sets";
import type { DtmGrid } from "@util/common_util_surface_sampler";
import {
extendUnclosed,
type CrossGroundPoint,
type ExtendInfo,
type UnclosedDesign,
} from "../../B05_Profile/logic/B05_Profile_Sections_Ground";
import { USER_TOUCHED_KEYS } from "./B06_Section_Design_Keys";
import { facilityAt, facilityRanges, type FacilityStructure } from "./B06_Section_Cross_Enforce";
type Raw = Record<string, unknown>;
/** 파이썬 `compute_cross_design` 키 → TS 짝 키(거울 시험 test_b06_cross_design_mirror 표와 같음). */
const CROSS_OPTION_KEYS: Record<string, keyof CrossDesignOptions> = {
ground_type: "groundType",
section_mode: "sectionMode",
ditch_side: "ditchSide",
ditch_type: "ditchType",
paved: "paved",
rock_boundary_offset_m: "rockBoundaryOffsetM",
two_stage_slope: "twoStageSlope",
cut_slope_ratio: "cutSlopeRatio",
ditch_enabled: "ditchEnabled",
ditch_choice: "ditchChoice",
surface_drop_m: "surfaceDropM",
plan_radius_m: "planRadiusM",
curve_outer_side: "curveOuterSide",
curve_widening_m: "curveWideningM",
shoulder_extend_left_m: "shoulderExtendLeftM",
shoulder_extend_right_m: "shoulderExtendRightM",
tilted: "tilted",
tilt_pct: "tiltPct",
};
/** 파이썬 BermSpec(NamedTuple → dict) → TS BermSpec. */
function bermOf(value: unknown): BermSpec | null {
if (!value || typeof value !== "object") return null;
const berm = value as Record<string, number | null>;
return {
widthM: berm.width_m as number,
intervalM: berm.interval_m as number,
slopeDeg: berm.slope_deg as number,
depthM: berm.depth_m as number,
depthLeftM: berm.depth_left_m ?? null,
depthRightM: berm.depth_right_m ?? null,
};
}
/** 파이썬 키 입력 → 엔진 입력(모르는 키 = 던짐). */
export function crossOptions(
raw: Raw,
standard: CrossDesignOptions["standard"],
): CrossDesignOptions {
const options: Raw = { standard };
for (const [key, value] of Object.entries(raw)) {
if (key === "berm") options.berm = bermOf(value);
else if (key in CROSS_OPTION_KEYS) options[CROSS_OPTION_KEYS[key]] = value;
else throw new Error(`모르는 횡단 설계 키: ${key}`);
}
return options as unknown as CrossDesignOptions;
}
const isNum = (value: unknown): value is number =>
typeof value === "number" && Number.isFinite(value);
/** 파이썬 `stored_cut_slope` — 측점별 암 절토 경사 · 없거나 0 = null. */
const storedCutSlope = (design: Raw): number | null =>
isNum(design.cut_slope_ratio_user) && design.cut_slope_ratio_user > 0
? design.cut_slope_ratio_user
: null;
/** 파이썬 `berm_from` — 소단 제원 dict + 쪽마다 옮긴 깊이 → BermSpec(python 키). */
function bermFrom(spec: unknown, sides: Raw): Raw | null {
if (!spec || typeof spec !== "object" || Array.isArray(spec)) return null;
const own = (side: string): number | null => {
const value = sides[`berm_depth_${side}_m`];
return isNum(value) ? Math.max(value, 0) : null;
};
const s = spec as Raw;
const field = (key: string, fallback: number): number => Number(s[key] ?? fallback);
const out = {
width_m: field("width_m", BERM_DEFAULT_WIDTH_M),
interval_m: field("interval_m", BERM_DEFAULT_INTERVAL_M),
slope_deg: field("slope_deg", BERM_DEFAULT_SLOPE_DEG),
depth_m: Math.max(field("depth_m", BERM_DEFAULT_DEPTH_M), 0),
};
if (!Object.values(out).every(Number.isFinite)) return null; // 파이썬 TypeError · ValueError
return { ...out, depth_left_m: own("left"), depth_right_m: own("right") };
}
const storedBerm = (design: Raw): Raw | null => bermFrom(design.berm, design);
/** 파이썬 `stored_shoulder_extend` — 없거나 음수 = 0. */
const storedShoulder = (design: Raw): Raw =>
Object.fromEntries(
(["left", "right"] as const).map((side) => {
const value = design[`shoulder_extend_${side}_m`];
return [`shoulder_extend_${side}_m`, isNum(value) ? Math.max(value, 0) : 0];
}),
);
/** 파이썬 `stored_tilt`. */
const storedTilt = (design: Raw): { tilted: boolean; tilt_pct: number | null } => ({
tilted: Boolean(design.tilted),
tilt_pct: isNum(design.tilt_pct) ? design.tilt_pct : null,
});
/** 파이썬 `curve_widening_args`. */
const curveArgs = (section: Raw): Raw => ({
plan_radius_m: section.plan_radius_m ?? null,
curve_outer_side: section.curve_outer_side ?? null,
curve_widening_m: section.curve_widening_m ?? null,
});
/** 파이썬 `design_elevation_from_longitudinal` — 계획선 표본 선형 보간 · 범위 밖 끝값 · 없으면 null. */
export function elevationAt(
samples: ReadonlyArray<{ chainage_m?: unknown; elevation_m?: unknown }>,
chainageM: number,
): number | null {
const rows = samples.filter((s) => isNum(s.elevation_m) && isNum(s.chainage_m)) as Array<{
chainage_m: number;
elevation_m: number;
}>;
if (!rows.length) return null;
if (chainageM <= rows[0].chainage_m) return rows[0].elevation_m;
const last = rows[rows.length - 1];
if (chainageM >= last.chainage_m) return last.elevation_m;
for (let index = 1; index < rows.length; index += 1) {
const [previous, current] = [rows[index - 1], rows[index]];
if (chainageM > current.chainage_m) continue;
const span = current.chainage_m - previous.chainage_m;
if (span <= 0) return current.elevation_m;
const ratio = (chainageM - previous.chainage_m) / span;
return previous.elevation_m + (current.elevation_m - previous.elevation_m) * ratio;
}
return last.elevation_m;
}
/** 저장분 공통 입력(파이썬 넷이 같은 꼴로 모음) — 포장 · 하강 · 기울임은 부르는 자리가 덮음. */
function storedOptions(design: Raw, section: Raw, rockDefaultM: number): Raw {
return {
ground_type: String(design.ground_type || "ripping_rock"),
section_mode: String(design.section_mode || "left_cut"),
ditch_side: design.ditch_side ?? null,
ditch_type: String(design.ditch_type || "standard"),
paved: Boolean(design.paved),
rock_boundary_offset_m:
"rock_boundary_offset_m" in design ? design.rock_boundary_offset_m : rockDefaultM,
two_stage_slope: Boolean(design.two_stage_slope),
cut_slope_ratio: storedCutSlope(design),
ditch_enabled: design.ditch_enabled ?? null,
ditch_choice: design.ditch_choice ?? null,
berm: storedBerm(design),
...curveArgs(section),
...storedShoulder(design),
};
}
/** 측점 하나(서버 상세 꼴) — 쓰는 칸만. */
export interface SaveSection {
chainage_m: number;
design?: Raw | null;
samples?: CrossGroundSample[];
frame?: { origin?: { x: number; y: number }; left_xy?: [number, number] };
plan_radius_m?: number | null;
curve_outer_side?: string | null;
curve_widening_m?: number | null;
}
export interface SaveCrossInput {
/** 종단 계획선 표본(`design_profiles[0].samples`). */
profile: ReadonlyArray<{ chainage_m?: unknown; elevation_m?: unknown }>;
sections: SaveSection[];
standard: CrossDesignOptions["standard"];
rockDefaultM: number;
pipes: readonly CulvertPipePoint[];
/** 구조물 정본 — null = 못 읽음(소단 = 저장분 그대로 · 포장 · 맹암거 구간 없음). */
structures: readonly FacilityStructure[] | null;
}
/** 측점마다 결과 — 설계 없는 측점 null · 넓혔으면 표본 · 넓힘 기록. */
export interface SaveCrossRow {
design: Raw;
samples?: CrossGroundSample[];
extended?: ExtendInfo;
}
/** [저장] 횡단 한 바퀴 — 파이썬 `_recompute` 의 설계선 · 넓힘 · 포장 · 하강 넷(차례 그대로 · 제자리 아님). */
export function enforceSavedCross(
input: SaveCrossInput,
grid: () => DtmGrid | null,
): Array<SaveCrossRow | null> {
const ranges = facilityRanges(input.pipes, input.structures ?? []);
const spans = input.structures
? bermSpansFromStructures(input.structures as Parameters<typeof bermSpansFromStructures>[0])
: null;
const rows = input.sections.map((section): SaveCrossRow | null =>
section.design && typeof section.design === "object" ? { design: section.design } : null,
);
const elevation = (section: SaveSection) => elevationAt(input.profile, section.chainage_m);
const compute = (samples: CrossGroundSample[] | undefined, z: number | null, raw: Raw) => {
try {
return computeCrossDesign(
samples ?? [],
z,
crossOptions(raw, input.standard),
) as unknown as Raw;
} catch {
return null; // 파이썬 ValueError · Node { error } 자리 — 저장분 그대로
}
};
/** 다시 세운 설계에 사용자 값 · 상태 승계(파이썬 `apply_designs`). */
const keep = (next: Raw, from: Raw): Raw => {
for (const key of ["status", "pavement_suggested", ...USER_TOUCHED_KEYS])
if (from[key] !== undefined && from[key] !== null) next[key] = from[key];
return next;
};
const at = (section: SaveSection) => facilityAt(ranges, section.chainage_m);
const sectionRaw = (section: SaveSection) => section as unknown as Raw;
// ① 모든 측점 설계선(`enforce_design_lines`) — 소단 = 구조물 정본 구간 · 하강 · 기울임 = 지금 시설
input.sections.forEach((section, index) => {
const row = rows[index];
if (!row) return;
const design = spans
? { ...row.design, berm: bermSpecAt(spans, section.chainage_m) }
: row.design;
const facility = at(section);
const next = compute(section.samples, elevation(section), {
...storedOptions(design, sectionRaw(section), input.rockDefaultM),
surface_drop_m: facility.surfaceDropM,
tilted: facility.tilted,
tilt_pct: facility.tiltPct,
});
if (next) row.design = keep(next, design);
});
// ② 미교차 넓힘(`extend_unclosed_sections`) — 열린 쪽만 +5 m · DTM 격자 · 넓힌 표본으로 다시 셈
const open = rows.some((row) => row?.design.slope_unclosed);
const map = open ? grid() : null;
input.sections.forEach((section, index) => {
const row = rows[index];
const origin = section.frame?.origin;
const left = section.frame?.left_xy;
if (!map || !row?.design.slope_unclosed || !origin || !left) return;
const design = row.design;
const z = elevation(section);
const raw = {
...storedOptions(design, sectionRaw(section), input.rockDefaultM),
surface_drop_m: at(section).surfaceDropM,
...storedTilt(design),
};
let wide;
try {
wide = extendUnclosed(
(section.samples ?? []) as Array<Partial<CrossGroundPoint>>,
[origin.x, origin.y],
left,
(samples) => {
const next = compute(samples as CrossGroundSample[], z, raw);
if (!next) throw new Error("넓힘 셈 못 함");
return next as unknown as UnclosedDesign;
},
map,
);
} catch {
return; // 파이썬 ValueError · KeyError — 그 측점 그대로
}
if (!wide || (!wide.info.left_m && !wide.info.right_m)) return;
const samples = wide.samples as CrossGroundSample[];
const next = compute(samples, z, raw);
if (!next) return;
rows[index] = { design: keep(next, design), samples, extended: wide.info };
});
const samplesOf = (index: number) => rows[index]?.samples ?? input.sections[index].samples;
// ③ 포장 구간(`enforce_pavement_ranges`) — 포장 · 기울임이 저장분과 다른 측점만
input.sections.forEach((section, index) => {
const row = rows[index];
if (!row) return;
const design = row.design;
const facility = at(section);
const tilt = storedTilt(design);
if (
Boolean(design.paved) === facility.paved &&
tilt.tilted === facility.tilted &&
tilt.tilt_pct === facility.tiltPct
)
return;
const next = compute(samplesOf(index), elevation(section), {
...storedOptions(design, sectionRaw(section), input.rockDefaultM),
paved: facility.paved,
surface_drop_m: facility.surfaceDropM,
tilted: facility.tilted,
tilt_pct: facility.tiltPct,
});
if (next) row.design = keep(next, design);
});
// ④ 세월교 노면 하강(`enforce_ford_surface_drops`) — 하강이 저장분과 다른 측점만
input.sections.forEach((section, index) => {
const row = rows[index];
if (!row) return;
const design = row.design;
const wanted = at(section).surfaceDropM;
if (Math.abs(wanted - Number(design.surface_drop_m || 0)) < 1e-6) return;
const next = compute(samplesOf(index), elevation(section), {
...storedOptions(design, sectionRaw(section), input.rockDefaultM),
surface_drop_m: wanted,
...storedTilt(design),
});
if (next) row.design = keep(next, design);
});
return rows;
}