/* ============================================================================= * B06_Section_Station_Swap.ts * 측점 수가 바뀔 때 횡단 목록 갈아끼움(PLAN 346-2 4파) — 계산 묶음 측점(`PipelineStation[]`) → 새 상세 객체. * * · 같은 측점(열쇠 · 없으면 같은 자리 같은 종류)은 앞 횡단을 이어 받음 — 설계 · 구조물 제원 · 이름 그대로 · * 자리(누가거리 · 좌표 · 방위 · 확폭)와 지반 표본(채워 왔으면)만 새것. * · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움). * · 빠진 측점 = 목록에서 뺌(세션 고침은 열쇠로 남음 · 노선이 바뀐 때만 「노선 밖 고침」 · 부르는 쪽). * · 옛 상세는 그대로 둠 — 새 객체를 돌려줌(쥔 쪽이 갈아끼움). * * ⚠ 파이썬·TS 짝 — `B05_Profile_Engine_Sections_Core.py` `format_station` ↔ `formatStation`. * 거울 시험 `resources/tester/test_346_2_station_swap.py`. * 순수 계산(DOM · fetch · 세션 없음). * ========================================================================== */ import type { PipelineStation } from "../../A00_Common/design/logic/design_pipeline_types"; import type { CrossSection, SectionDetailResponse, SectionStation } from "../B06_Section_Api_Fetch"; interface Sample { offset_m: number; elevation_m: number | null; valid: boolean; } /** 지반 표본(중심 → 바깥 · 좌 +) → 엔진 표본(오프셋 오름차순 · 우 −). */ export function groundSamples(station: PipelineStation): Sample[] { const ground = station.ground; if (!ground) return []; const point = (offset: number, z: number | null): Sample => ({ offset_m: offset, elevation_m: z, valid: z !== null && Number.isFinite(z), }); const right = ground.right.map((s) => point(-s.offsetM, s.z)); const left = ground.left.map((s) => point(s.offsetM, s.z)); const out = [...right, ...left]; if (ground.centerZ !== null && !out.some((s) => Math.abs(s.offset_m) < 1e-9)) out.push(point(0, ground.centerZ)); return out.sort((a, b) => a.offset_m - b.offset_m); } /** 짝: `format_station` — `STA.k+mmm.mmm`. */ export function formatStation(chainageM: number): string { const at = Math.max(chainageM, 0); const km = Math.floor(at / 1000); const rest = (at - km * 1000).toFixed(3).padStart(7, "0"); return `STA.${km}+${rest}`; } /** 서버 측점 id 꼴(`station_000000484210` = 누가거리 mm 12자리). */ export const stationIdOf = (chainageM: number): string => `station_${String(Math.round(chainageM * 1000)).padStart(12, "0")}`; /** 새 측점의 기본 설계 — 계획고 0 이라 늘 낡음 → 다시 셈이 세션 선택 · 기본값으로 채움. */ const freshDesign = () => ({ design_elevation_m: 0, ground_type: "ripping_rock", section_mode: "left_cut", ditch_type: "standard", }) as unknown as NonNullable; function kindOf(station: PipelineStation): SectionStation["kind"] { if (station.kind === "end") return station.key.endsWith("EP") ? "ep" : "bp"; return station.kind === "grid" ? "regular" : "irregular"; } /** 같은 목록인가 — 열쇠 · 누가거리가 차례대로 같음(제자리 갱신이면 갈아끼우지 않음). */ export function sameStationList( sections: ReadonlyArray<{ chainage_m: number }>, oldKeys: ReadonlyArray, stations: ReadonlyArray, ): boolean { return ( sections.length === stations.length && stations.every( (station, index) => Math.abs(station.chainageM - sections[index].chainage_m) < 1e-6 && (oldKeys[index] === station.key || kindOf(station) === "regular"), ) ); } /** 조작값 맵 열쇠(`85.05` · `85.05:outlet` · `b85.05`)의 누가거리를 새 자리로 — 안 옮기는 열쇠는 그대로. */ export function renameChainageKeys( values: ReadonlyMap, moves: ReadonlyMap, ): Map { const out = new Map(); for (const [key, value] of values) { const match = /^(b?)(-?\d+\.\d{2})(:.*)?$/.exec(key); const to = match && moves.get(match[2]); out.set(to ? `${match[1]}${to}${match[3] ?? ""}` : key, value); } return out; } export interface StationSwap { detail: SectionDetailResponse; added: string[]; removed: string[]; /** 이어 받은 측점 중 누가거리가 바뀐 것 — 옛 0.01 m 글 → 새 글(조작값 맵 옮김). */ moves: Map; /** 이어 받은 측점 중 좌표가 0.01 m 넘게 움직인 것이 있나 — 노선이 바뀐 차례(「노선 밖 고침」 셈). */ moved: boolean; } /** * 새 측점 목록으로 상세를 새로 짬. `oldKeys` = 지금 상세 측점마다 열쇠(같은 차례 · `stationKeysOf`) · * 열쇠가 안 맞으면 같은 자리(0.01 m) · 같은 종류의 앞 횡단을 씀. */ export function swapCrossSections( detail: SectionDetailResponse, stations: ReadonlyArray, oldKeys: ReadonlyArray, intervalM: number | null, ): StationSwap { const byKey = new Map(detail.cross_sections.map((section, i) => [oldKeys[i], section])); const used = new Set(); let moved = false; const added: string[] = []; const moves = new Map(); const text = (chainageM: number) => chainageM.toFixed(2); const priorOf = (station: PipelineStation): CrossSection | undefined => { const hit = byKey.get(station.key); if (hit && !used.has(hit)) return hit; return detail.cross_sections.find( (section) => !used.has(section) && section.kind === kindOf(station) && Math.abs(section.chainage_m - station.chainageM) < 0.01, ); }; const longStations: SectionStation[] = []; const cross_sections = stations.map((station): CrossSection => { const prior = priorOf(station); if (prior) used.add(prior); else added.push(station.key); if (prior && Math.hypot(prior.center_x - station.x, prior.center_y - station.y) > 0.01) moved = true; if (prior && text(prior.chainage_m) !== text(station.chainageM)) moves.set(text(prior.chainage_m), text(station.chainageM)); const kind = kindOf(station); const onGrid = kind === "irregular" && !!intervalM && Math.abs(station.chainageM / intervalM - Math.round(station.chainageM / intervalM)) < 1e-6; const place = { station_id: prior?.station_id ?? stationIdOf(station.chainageM), chainage_m: station.chainageM, label: formatStation(station.chainageM), kind, ...(onGrid ? { grid: true } : {}), center_z: station.ground?.centerZ ?? prior?.center_z ?? null, azimuth_deg: station.azimuthDeg, plan_radius_m: station.planRadiusM, curve_outer_side: station.curveOuterSide, curve_widening_m: station.curveWideningM, center_x: station.x, center_y: station.y, frame: { left_xy: [station.left[0], station.left[1]] as [number, number] }, }; longStations.push({ ...place, ...(prior?.structure ? { structure: prior.structure } : {}) }); const samples = station.ground ? groundSamples(station) : (prior?.samples ?? []); return prior ? { ...prior, ...place, samples } : { ...place, samples, design: freshDesign() }; }); const removed = detail.cross_sections .map((section, i) => (used.has(section) ? null : oldKeys[i])) .filter((key): key is string => key !== null); const last = stations[stations.length - 1]?.chainageM ?? detail.longitudinal.length_m; return { detail: { ...detail, longitudinal: { ...detail.longitudinal, stations: longStations, length_m: last }, cross_sections, }, added, removed, moved, moves, }; }