/* ============================================================================= * common_util_structure_pavement.ts * 노면공(G군) 구조물 → 횡단 측점 포장 제원(`section.pavement`). * 파이썬 `common_util/common_util_structure_pavement.py` 의 **짝** — 거울 시험 * `resources/tester/test_b06_structure_pavement_mirror.py`. * * 서버는 저장분으로 · 브라우저는 저장 전 목록으로 같은 제원을 얹음. * 두께 = options.height_m · 무늬 = options.pave_spec · 확폭 끈 구간 = options.widening_off. * ========================================================================== */ /** 구조물 정본 한 건(필요한 칸만) — `structures.json` 과 같은 이름. */ export interface PavementStructureInput { type_id: string; chainage_m?: number | null; start_m?: number | null; end_m?: number | null; options?: Record | null; } /** 측점에 얹는 포장 한 건 — `section.pavement[]`. */ export interface PavementEntry { type_id: string; /** 포장사양 — 콘크리트 · 아스콘 · 쇄석(비면 표준 그림). */ spec: string | null; /** 두께(m) — 노면선 아래로. 비면 표준 횡단 포장 두께. */ thickness_m: number | null; start_m: number; end_m: number; /** false = 확폭 끈 구간(규격 폭 끝까지만). */ widening_on: boolean; } export interface PavementSpec extends Omit { widening_off: Array<[number, number]>; } /** 파이썬 `_EDGE_TOLERANCE_M` — C군 벽과 같은 값. */ const EDGE_TOLERANCE_M = 0.02; const num = (value: unknown): number | null => typeof value === "number" && Number.isFinite(value) ? value : null; /** 「시작-끝,시작-끝」(측점 m) → 구간 목록. 못 읽는 조각은 버림(= 켬). */ export function wideningOffRanges(text: unknown): Array<[number, number]> { const ranges: Array<[number, number]> = []; for (const part of String(text ?? "").split(",")) { const bounds = part.split("-"); if (bounds.length !== 2 || !bounds[0].trim() || !bounds[1].trim()) continue; const start = Number(bounds[0]); const end = Number(bounds[1]); if (Number.isFinite(start) && Number.isFinite(end)) ranges.push([Math.min(start, end), Math.max(start, end)]); } return ranges; } /** G군 구조물 → 구간 제원(파이썬 `pavement_specs_from`). */ export function pavementSpecsFrom( structures: readonly PavementStructureInput[], typeIds: ReadonlySet, ): PavementSpec[] { const specs: PavementSpec[] = []; for (const item of structures) { if (!typeIds.has(item.type_id)) continue; const anchor = num(item.chainage_m); const start = num(item.start_m) ?? anchor; const end = num(item.end_m) ?? anchor; if (start === null || end === null) continue; const options = (item.options ?? {}) as Record; const spec = options.pave_spec; specs.push({ type_id: item.type_id, spec: typeof spec === "string" && spec ? spec : null, thickness_m: num(options.height_m), start_m: Math.min(start, end), end_m: Math.max(start, end), widening_off: wideningOffRanges(options.widening_off), }); } return specs; } const inside = (chainage: number, start: number, end: number): boolean => start - EDGE_TOLERANCE_M <= chainage && chainage <= end + EDGE_TOLERANCE_M; /** 범위 안 측점에 `pavement` 목록을 얹음(파이썬 `attach_pavement_specs`). 얹은 측점 수. */ export function attachPavementSpecs( sections: Array>, specs: readonly PavementSpec[], ): number { let attached = 0; for (const section of sections) { const chainage = num(section.chainage_m); if (chainage === null) continue; const found: PavementEntry[] = specs .filter((spec) => inside(chainage, spec.start_m, spec.end_m)) .map((spec) => ({ type_id: spec.type_id, spec: spec.spec, thickness_m: spec.thickness_m, start_m: spec.start_m, end_m: spec.end_m, widening_on: !spec.widening_off.some(([start, end]) => inside(chainage, start, end)), })); if (found.length) { section.pavement = found; attached += 1; } } return attached; }