Files
Aislo/common_util/common_util_structure_pavement.ts
T

112 lines
4.2 KiB
TypeScript

/* =============================================================================
* 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<string, unknown> | 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<PavementEntry, "widening_on"> {
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<string>,
): 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<string, unknown>;
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<Record<string, unknown>>,
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;
}