"""계획노선 폴리라인 파일 쓰기(`B05_Profile_Router_Replan` 에서 나눔 · PLAN 511-B B-5). 노드 · 곡선 켬끔 · R(편집 갈래) → 선 · 노드 · 곡선표(+ 편집표) = 늘 떠 있는 Node `route.polyline` (화면 `buildEditedPolyline` · `nodeEdits` 와 같은 TS) · 파이썬은 파일 꼴(csv 4자리 · 곡선 as_dict 반올림)만. 단순화 갈래(예상노선 첫 폴리라인) · Node 못 돌림 = 파이썬 `build_planned_polyline` · `node_edits`(짝 · 로그 남김). """ from __future__ import annotations import json import logging from pathlib import Path from typing import Any from B05_Profile.B05_Profile_Route_NodeEdits import node_edits from common_util.common_util_route_geometry import write_route_csv from common_util.common_util_route_polyline import ( PlannedPolyline, RouteCurve, RouteNode, build_planned_polyline, ) logger = logging.getLogger(__name__) def _nodes_path(path: Path) -> Path: """그 폴리라인을 낳은 **노드** 파일 자리 — `planned_route.csv` → `planned_route_nodes.csv`.""" return path.with_name(f"{path.stem}_nodes.csv") def _curves_path(path: Path) -> Path: """그 폴리라인의 **곡선 성분** 자리 — `planned_route.csv` → `planned_route_curves.json`. 왜 따로 남기나(2026-09-07 사용자 확정) — 계획노선은 「직선 > 곡선 > 직선」이고 사용자가 잡는 것은 **곡선 시작·끝점**이며 반지름도 직접 바꾼다. 정점 목록만으로는 어디부터 어디까지가 한 곡선인지, 그 반지름이 얼마인지 알 수 없어 편집·도면이 같은 값을 못 본다. """ return path.with_name(f"{path.stem}_curves.json") def _read_curves(path: Path, key: str = "curves") -> list[dict]: """저장해 둔 곡선 성분(`key` = "edits" 면 넣은 편집값 · 430). 없거나 못 읽으면 빈 목록.""" target = _curves_path(path) if not target.is_file(): return [] try: data = json.loads(target.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): logger.warning("계획노선 곡선 성분을 읽지 못했습니다: %s", target) return [] curves = data.get(key) if isinstance(data, dict) else None return curves if isinstance(curves, list) else [] def node_polyline( points: list[tuple[float, float]], radius_m: float, curve_flags: list[bool] | None, radii: list[float | None] | None, locks: list[str | None] | None, arcs: list[float | None] | None, ) -> tuple[PlannedPolyline, list[dict[str, Any]]] | None: """편집 갈래 폴리라인 + 편집표 = Node `route.polyline` — 못 돌림 None.""" from common_util.common_util_calc_runner import run_calc route = { "points": [[float(x), float(y)] for x, y in points], "curve_flags": curve_flags, "radii": radii, "locks": locks, "arcs": arcs, "min_radius_m": float(radius_m), } try: out = (run_calc(["route.polyline"], {"route_polyline": route}) or {}).get("route.polyline") except Exception: # noqa: BLE001 — 노선 저장은 파이썬 짝으로 이어 감 logger.exception("Node 노선 폴리라인 실패 — 파이썬 길") return None if not out: logger.warning("Node 노선 폴리라인 못 돌림 — 파이썬 길") return None # JSON 정수(100) → float — 파이썬 길과 같은 글(`100.0`)로 적힘 nodes = [ RouteNode( x=float(x), y=float(y), inner_angle_deg=_num(info["inner_angle_deg"]), radius_m=_num(info["radius_m"]), tangent_m=_num(info["tangent_m"]), violations=list(info["violations"]), ) for (x, y), info in zip(out["cleaned"], out["nodes"]) ] curves = [ RouteCurve( apex=_xy(curve["apex"]), radius_m=float(curve["radius_m"]), tangent_m=float(curve["tangent_m"]), inner_angle_deg=float(curve["inner_angle_deg"]), start=_xy(curve["start"]), end=_xy(curve["end"]), node_first=int(curve["node_first"]), node_last=int(curve["node_last"]), violations=list(curve["violations"]), ) for curve in out["curves"] ] vertices = [_xy(vertex) for vertex in out["vertices"]] edits = [ {**edit, "radius_m": _num(edit["radius_m"]), "arc_m": _num(edit["arc_m"])} for edit in out["edits"] ] return PlannedPolyline(nodes=nodes, vertices=vertices, curves=curves), edits def _num(value: Any) -> float | None: return None if value is None else float(value) def _xy(pair: list[float]) -> tuple[float, float]: return float(pair[0]), float(pair[1]) def _write_planned_polyline( path: Path, points: list[tuple[float, float]], radius_m: float, *, simplify: bool = True, curve_flags: list[bool] | None = None, radii: list[float | None] | None = None, station_snap: bool | None = None, station_snap_step: float | None = None, locks: list[str | None] | None = None, arcs: list[float | None] | None = None, ) -> dict: """점 묶음을 폴리라인으로 바꿔 CSV 로 쓴다. 노드 요약을 돌려준다. `station_snap` — [확인] 때만 곡선표에 적음(요구 224-2). None 이면 칸을 안 둠(끔) · `station_snap_step` — 켬 + 간격이 있을 때만 곡선표에 적음(390-5 · 없으면 측점 No.). `locks` · `arcs` — 편집값이 오면 넣은 값을 곡선표 `edits` 에(430 · `_Route_NodeEdits`). **노드도 함께 남긴다** — 노드는 폴리라인에서 되뽑을 수 없다. 폴리라인에는 원호 위 점이 섞여 있어 다시 단순화하면 꺾임점이 조금씩 지워지고, 그것을 반복하면 [확인]을 누를 때마다 선이 깎인다(2026-09-07 실측: 정점 136 → 134 → 119). 낳은 값을 그대로 보관한다. """ edited = curve_flags is not None or radii is not None made = ( node_polyline(points, radius_m, curve_flags, radii, locks, arcs) if edited and not simplify else None ) if made is not None: result, edits = made else: result = build_planned_polyline( points, min_radius_m=radius_m, simplify=simplify, curve_flags=curve_flags, radii=radii, ) edits = node_edits(points, curve_flags, radii, locks, arcs) if edited else None write_route_csv(path, [{"x": x, "y": y} for x, y in result.vertices]) write_route_csv(_nodes_path(path), [{"x": node.x, "y": node.y} for node in result.nodes]) table: dict[str, Any] = { "min_radius_m": round(radius_m, 3), "curves": [c.as_dict() for c in result.curves], } if station_snap is not None: table["station_snap"] = bool(station_snap) if station_snap and station_snap_step is not None: table["station_snap_step"] = float(station_snap_step) if edits is not None: table["edits"] = edits _curves_path(path).write_text(json.dumps(table, ensure_ascii=False), encoding="utf-8") return { "nodes": len(result.nodes), "curves": result.curve_count, "violations": result.violation_count, "vertices": len(result.vertices), }