"""측점 열쇠 한 규칙(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:<누가거리>``(``dedupe_station_keys``). MC 가 움직여 곡선 열쇠가 바뀌면 넘길 때 가까운 새 곡선 측점으로 이음(``curve_moves`` · 432-3). ⚠ 파이썬·TS 짝 — ``common_util_station_key.ts`` ``stationKey`` ↔ ``station_key``. 한쪽을 고치면 다른 쪽도 고치고 거울 시험 ``resources/tester/test_344_3_station_key_mirror.py``. """ from __future__ import annotations import math import re _LOOSE = re.compile(r"^-?\d+(\.\d+)?$") def _half_up(value: float) -> int: """JS ``Math.round`` 와 같은 반올림(0.5 는 위로).""" return math.floor(value + 0.5) def chainage_key(chainage_m: float) -> str: """누가거리 0.01 m — 옛 초안 열쇠(``toFixed(2)``)와 같은 자릿수.""" return f"{_half_up(chainage_m * 100) / 100:.2f}" def station_key( kind: str, chainage_m: float, interval_m: float | None, station_id: str | None = None, id_kind: str = "structure", role: str = "anchor", ) -> str: """측점 열쇠 — irregular 는 id 가 있으면 pipe: · structure:(``id_kind``) · 없으면 at:. 구간형 시작 · 끝은 ``role`` 로 ``:start`` · ``:end``. ``grid`` · ``end`` 는 ``station_id`` 에 원래 격자 번호 · BP/EP(없으면 누가거리 ÷ 간격 반올림 · 0 이면 BP). """ loose = f"at:{chainage_key(chainage_m)}" suffix = "" if role == "anchor" else f":{role}" if kind == "bp": return "end:BP" if kind == "ep": return "end:EP" if kind == "end": return ( f"end:{station_id if station_id is not None else ('BP' if chainage_m <= 0 else 'EP')}" ) if kind == "curve": return f"curve:{chainage_key(chainage_m)}" if kind in ("pipe", "structure"): return f"{kind}:{station_id}{suffix}" if station_id else loose if kind == "irregular": return f"{id_kind}:{station_id}{suffix}" if station_id else loose if kind == "grid" and station_id is not None: return f"grid:{station_id}" if interval_m and interval_m > 0: return f"grid:{_half_up(chainage_m / interval_m)}" return loose def dedupe_station_keys(keys: list[str], chainages: list[float]) -> list[str]: """한 목록에 같은 열쇠가 둘이면 둘째부터 ``at:<누가거리>`` — 열쇠마다 측점 하나.""" seen: set[str] = set() out: list[str] = [] for key, chainage in zip(keys, chainages, strict=True): unique = f"at:{chainage_key(chainage)}" if key in seen else key seen.add(unique) out.append(unique) return out def loose_chainage(key: str) -> float | None: """옛 초안 열쇠(누가거리 글) · ``at:`` 열쇠의 누가거리 — 측점 열쇠면 None.""" text = key[3:] if key.startswith("at:") else key return float(text) if _LOOSE.match(text) else None #: 옮긴 곡선 측점으로 보는 거리(m) — 옛 MC 자리에서 이 안의 새 MC. CURVE_NEAR_M = 10.0 def curve_moves(before: list[tuple[str, float]], after: list[tuple[str, float]]) -> dict[str, str]: """MC 가 움직여 바뀐 ``curve:`` 열쇠 → 가까운 새 곡선 열쇠. 옛 목록에만 있는 곡선 열쇠 → 새 목록에만 있는 가장 가까운 곡선 열쇠(``CURVE_NEAR_M`` 안 · 새 열쇠 하나에 옛 열쇠 하나 · 옛 누가거리 차례). """ was = {key for key, _ in before} kept = {key for key, _ in after} free = [(key, at) for key, at in after if key.startswith("curve:") and key not in was] out: dict[str, str] = {} for key, at in sorted(before, key=lambda item: item[1]): if not key.startswith("curve:") or key in kept: continue best = -1 for index, (_, to) in enumerate(free): gap = abs(to - at) if gap <= CURVE_NEAR_M and (best < 0 or gap < abs(free[best][1] - at)): best = index if best >= 0: out[key] = free.pop(best)[0] return out #: 같은 자리로 보는 중심 차(m) · 축 차 — 브라우저 틀 ↔ 서버 틀 차(467)를 넘김. SEAT_XY_M = 0.01 SEAT_AXIS = 1e-3 def station_seat(station: dict | None) -> tuple[float, float, tuple[float, float] | None] | None: """종단 측점 줄(``center_x`` · ``center_y`` · ``frame.left_xy``) → 자리 · 모르면 None.""" if not isinstance(station, dict): return None try: x, y = float(station["center_x"]), float(station["center_y"]) except (KeyError, TypeError, ValueError): return None left = (station.get("frame") or {}).get("left_xy") axis = (float(left[0]), float(left[1])) if isinstance(left, list) and len(left) == 2 else None return x, y, axis def same_seat(a, b) -> bool: """노선이 바뀐 뒤 그 측점이 같은 자리인가(요구 275-3) — 짝 ``sameSeat`` · 거울 475-3. 자리 = (x, y, 왼쪽 단위 벡터 | None) · 한쪽을 모르면 같은 자리로 봄(옛 동작). """ if a is None or b is None: return True if math.hypot(a[0] - b[0], a[1] - b[1]) > SEAT_XY_M: return False if a[2] is None or b[2] is None: return True return abs(a[2][0] - b[2][0]) <= SEAT_AXIS and abs(a[2][1] - b[2][1]) <= SEAT_AXIS