/* ============================================================================= * common_util_station_key.ts * 측점 열쇠 한 규칙(PLAN 344-3) — 노선이 바뀌어도 같은 측점을 같은 이름으로 부름. * 일반 = `grid:k`(누가거리 ÷ 측점 간격 반올림) · 관 = `pipe:` · 구조물 = `structure:` · * 시종점 = `end:BP` · `end:EP` · id 를 모르는 구조물 측점 = `at:<누가거리 0.01 m>`(옛 초안 열쇠와 같은 몫). * 곡선 측점(MC · 252-2) = `curve:<누가거리 0.01 m>`. * 구간형 구조물의 시작 · 끝 측점 = `structure::start` · `:end`(기준 측점은 `structure:` 그대로). * 한 목록에 같은 열쇠가 둘이면 둘째부터 `at:<누가거리>`(`dedupeStationKeys`). * MC 가 움직여 곡선 열쇠가 바뀌면 넘길 때 가까운 새 곡선 측점으로 이음(`curveMoves` · 432-3). * 쓰는 곳 = 서버 `B05_Profile_Route_Save_Parts` · 묶음 stations 단계 · B06 세션 횡단 초안(`stationKeysOf`). * * ⚠ 파이썬·TS 짝 — `common_util_station_key.py` `station_key` ↔ `stationKey`. 한쪽을 고치면 다른 쪽도 * 고치고 거울 시험 `resources/tester/test_344_3_station_key_mirror.py`. * ========================================================================== */ /** 측점 종류 — 서버 측점 `kind`(bp · ep · regular · irregular · curve) · 묶음 측점 `kind`(grid · end · pipe · structure · curve). */ export type StationKeyKind = "bp" | "ep" | "regular" | "irregular" | "grid" | "end" | "pipe" | "structure" | "curve"; /** 구간형 구조물 측점 자리 — 기준(anchor) · 시작 · 끝. */ export type StationKeyRole = "anchor" | "start" | "end"; /** 누가거리 0.01 m — 옛 초안 열쇠(`toFixed(2)`)와 같은 자릿수. */ export const chainageKey = (chainageM: number): string => (Math.round(chainageM * 100) / 100).toFixed(2); /** * 측점 열쇠. irregular 는 id 가 있으면 `pipe:` · `structure:`(종류는 `idKind`) · 없으면 `at:` · 구간형 시작 · 끝은 * `role` 로 `:start` · `:end` 덧붙임. `grid` · `end` 는 `id` 에 원래 격자 번호 · `BP`/`EP` 를 줄 수 있음(없으면 * 누가거리 ÷ 간격 반올림 · 0 이면 BP). 간격이 없거나 0 이하면 일반 측점도 `at:`. */ export function stationKey( kind: StationKeyKind, chainageM: number, intervalM: number | null, id: string | null = null, idKind: "pipe" | "structure" = "structure", role: StationKeyRole = "anchor", ): string { const loose = `at:${chainageKey(chainageM)}`; const suffix = role === "anchor" ? "" : `:${role}`; if (kind === "bp") return "end:BP"; if (kind === "ep") return "end:EP"; if (kind === "end") return `end:${id ?? (chainageM <= 0 ? "BP" : "EP")}`; if (kind === "curve") return `curve:${chainageKey(chainageM)}`; if (kind === "pipe" || kind === "structure") return id ? `${kind}:${id}${suffix}` : loose; if (kind === "irregular") return id ? `${idKind}:${id}${suffix}` : loose; if (kind === "grid" && id !== null) return `grid:${id}`; if (intervalM && intervalM > 0) return `grid:${Math.round(chainageM / intervalM)}`; return loose; } /** 한 목록에 같은 열쇠가 둘이면 둘째부터 `at:<누가거리>` — 열쇠마다 측점 하나. */ export function dedupeStationKeys( keys: ReadonlyArray, chainages: ReadonlyArray, ): string[] { const seen = new Set(); return keys.map((key, index) => { const unique = seen.has(key) ? `at:${chainageKey(chainages[index])}` : key; seen.add(unique); return unique; }); } /** 옛 초안 열쇠(누가거리 글 `"85.05"`) · `at:` 열쇠의 누가거리 — 측점 열쇠면 null. */ export function looseChainage(key: string): number | null { const text = key.startsWith("at:") ? key.slice(3) : key; if (!/^-?\d+(\.\d+)?$/.test(text)) return null; return Number(text); } /** 옮긴 곡선 측점으로 보는 거리(m) — 옛 MC 자리에서 이 안의 새 MC. */ export const CURVE_NEAR_M = 10; /** * MC 가 움직여 `curve:` 열쇠가 바뀜 — 옛 목록에만 있는 곡선 열쇠 → 새 목록에만 있는 가장 가까운 곡선 열쇠 * (`CURVE_NEAR_M` 안 · 새 열쇠 하나에 옛 열쇠 하나 · 옛 누가거리 차례). 목록 = [열쇠, 누가거리]. */ export function curveMoves( before: ReadonlyArray, after: ReadonlyArray, ): Map { const isCurve = (key: string) => key.startsWith("curve:"); const was = new Set(before.map(([key]) => key)); const kept = new Set(after.map(([key]) => key)); const free = after.filter(([key]) => isCurve(key) && !was.has(key)); const out = new Map(); for (const [key, at] of [...before].sort((a, b) => a[1] - b[1])) { if (!isCurve(key) || kept.has(key)) continue; let best = -1; free.forEach(([, to], i) => { const gap = Math.abs(to - at); if (gap <= CURVE_NEAR_M && (best < 0 || gap < Math.abs(free[best][1] - at))) best = i; }); if (best >= 0) out.set(key, free.splice(best, 1)[0][0]); } return out; }