"""노선이 바뀔 때 계획고 옮김 — 바뀐 구간만 지반에 다시 맞추고 그 밖 편집은 길이 차만큼 민다(344-2). 사용자 확정(10-16 · 341 결정 1): 노선 끝쪽을 고쳤는데 앞쪽 계획고가 날아가지 않게 한다. - 바뀐 구간 = 옛 · 새 노선 정점열의 같은 앞머리 · 같은 꼬리를 뺀 가운데(`changed_span`) — 바뀐 노드 앞 · 뒤 접선점 사이. 원호 정점까지 같은지 보므로 곡선이 바뀐 이웃 노드 범위도 들어간다. - 구간 앞 = 같은 땅 · 같은 누가거리 → 그대로. 구간 뒤 = 같은 땅 · 누가거리만 길이 차만큼 밀림. - 구간 안 = 새 지반 첫 계획선(`design_ground_following_profile` 의 base_pvi) 꼭짓점으로 갈아 끼움 · 그 안 편집(계획고 · 종단곡선 R)은 버림. - 옮기는 것 = base_pvi · station_offsets · curve_radii(누가거리 글자 키). 짝 = TS `B05_Profile/logic/B05_Profile_Grade_Carry.ts`(같은 규칙) · 거울 시험 = `resources/tester/test_344_2_route_pairs.py`. 서버 정본은 이쪽(346-3 [저장]이 부름). """ from __future__ import annotations import math from typing import Any from B05_Profile.B05_Profile_Engine_Grade_Alignment import chainage_key # 같은 정점으로 볼 거리(m) — 저장 좌표 0.0001 m 자리 · 원호 다시 뽑기 잡음보다 크게. SAME_VERTEX_M = 1e-3 _EPS = 1e-6 def _cumulative(xy: list[tuple[float, float]]) -> list[float]: total = [0.0] for (x0, y0), (x1, y1) in zip(xy, xy[1:]): total.append(total[-1] + math.hypot(x1 - x0, y1 - y0)) return total def _same(a: tuple[float, float], b: tuple[float, float]) -> bool: return abs(a[0] - b[0]) <= SAME_VERTEX_M and abs(a[1] - b[1]) <= SAME_VERTEX_M def changed_span( old_xy: list[tuple[float, float]], new_xy: list[tuple[float, float]] ) -> dict[str, Any] | None: """옛 · 새 노선 정점열 → 바뀐 누가거리 구간. 같으면 None. `start_m` = 같은 앞머리 끝(옛 = 새) · `old_end_m` · `new_end_m` = 같은 꼬리 시작 · `delta_m` = 새 연장 − 옛 연장 · `start_fixed` · `end_fixed` = 앞머리 · 꼬리가 한 점이라도 같은가. """ old_xy = [(float(x), float(y)) for x, y in old_xy] new_xy = [(float(x), float(y)) for x, y in new_xy] limit = min(len(old_xy), len(new_xy)) prefix = 0 while prefix < limit and _same(old_xy[prefix], new_xy[prefix]): prefix += 1 if prefix == len(old_xy) == len(new_xy): return None suffix = 0 while suffix < limit - prefix and _same(old_xy[-1 - suffix], new_xy[-1 - suffix]): suffix += 1 old_cum, new_cum = _cumulative(old_xy), _cumulative(new_xy) return { "start_m": old_cum[prefix - 1] if prefix else 0.0, "old_end_m": old_cum[len(old_xy) - suffix] if suffix else old_cum[-1], "new_end_m": new_cum[len(new_xy) - suffix] if suffix else new_cum[-1], "delta_m": new_cum[-1] - old_cum[-1], "start_fixed": prefix > 0, "end_fixed": suffix > 0, } def _before(span: dict[str, Any], s: float) -> bool: """구간 앞(그대로 둠) — 앞머리가 같으면 그 끝점까지.""" start = float(span["start_m"]) return s < start - _EPS or (bool(span["start_fixed"]) and s <= start + _EPS) def _after(span: dict[str, Any], s: float, end: float) -> bool: """구간 뒤(밂) — 꼬리가 같으면 그 시작점부터.""" return s > end + _EPS or (bool(span["end_fixed"]) and s >= end - _EPS) def carry_profile_edits( base_pvi: list[dict[str, float]], edits: dict[str, Any] | None, span: dict[str, Any], ground_pvi: list[dict[str, float]], ) -> tuple[list[dict[str, float]], dict[str, dict[str, float]]]: """옛 꼭짓점 · 편집을 새 노선으로 옮긴다 → (새 base_pvi, 새 edits). `ground_pvi` = 새 노선 지반 첫 계획선 꼭짓점(전 구간 · `design_ground_following_profile`) — 바뀐 구간 안 몫만 쓴다. """ delta = float(span["delta_m"]) old_end, new_end = float(span["old_end_m"]), float(span["new_end_m"]) def moved(s: float) -> float | None: if _before(span, s): return s if _after(span, s, old_end): return s + delta return None # 바뀐 구간 안 — 버림 nodes: dict[float, dict[str, float]] = {} for node in base_pvi: at = moved(float(node["chainage_m"])) if at is not None: key = round(at, 3) nodes[key] = {"chainage_m": round(at, 6), "elevation_m": float(node["elevation_m"])} for node in ground_pvi: s = float(node["chainage_m"]) if _before(span, s) or _after(span, s, new_end): continue nodes.setdefault( round(s, 3), {"chainage_m": round(s, 6), "elevation_m": float(node["elevation_m"])} ) carried: dict[str, dict[str, float]] = {} for name in ("station_offsets", "curve_radii"): values: dict[str, float] = {} for key, value in ((edits or {}).get(name) or {}).items(): try: at = moved(float(key)) except (TypeError, ValueError): continue if at is not None and value is not None: values[chainage_key(at)] = float(value) carried[name] = values return [nodes[key] for key in sorted(nodes)], carried