"""추가 측점 정본(`B05_Profile/route/plus_stations.json`) — 구조물 정본 곁(368-2 · 요구 240-21). 사용자가 종단 원지반선에서 넣은 일반 측점(정수 m) · 이름 없음. 자리 정본 = 좌표(x · y) — 노선이 바뀌면 관 · 구조물처럼 새 노선에 투영해 누가거리를 다시 매김(`follow_structures` 재사용). 측점 계획에는 `resolve_extra_stations` → `plan_stations(plus=)` 로 들어감(TS `planStations` 짝). """ from __future__ import annotations import json import os from pathlib import Path from types import SimpleNamespace from typing import Any, Sequence from common_util.common_util_json import atomic_write_json from common_util.common_util_route_follow import follow_structures PLUS_STATIONS_FILE_NAME = "plus_stations.json" def plus_stations_path(project_root: str | Path) -> Path: return Path(project_root) / "B05_Profile" / "route" / PLUS_STATIONS_FILE_NAME def load_plus_stations(project_root: str | Path) -> list[dict[str, Any]]: """[{chainage_m, x, y}] — 없거나 깨졌으면 빈 목록.""" path = plus_stations_path(project_root) if not path.is_file(): return [] try: rows = json.loads(path.read_text(encoding="utf-8")).get("stations") or [] except (OSError, ValueError, AttributeError): return [] return [ row for row in rows if isinstance(row, dict) and isinstance(row.get("chainage_m"), (int, float)) ] def plus_chainages(project_root: str | Path) -> list[float]: return sorted({float(row["chainage_m"]) for row in load_plus_stations(project_root)}) def save_plus_stations( project_root: str | Path, chainages: Sequence[float], xy: Sequence[Sequence[float]] | None ) -> list[float]: """정수 m 로 맞춰(0 ~ 노선 끝 · 같은 m 하나) 좌표와 함께 씀 → 쓴 누가거리.""" from shapely.geometry import LineString line = LineString([(float(p[0]), float(p[1])) for p in xy]) if xy and len(xy) > 1 else None end = float(line.length) if line is not None else float("inf") values = sorted({float(round(c)) for c in chainages if 0 <= round(c) <= end}) rows = [] for value in values: point = line.interpolate(value) if line is not None else None rows.append( { "chainage_m": value, "x": round(float(point.x), 4) if point is not None else None, "y": round(float(point.y), 4) if point is not None else None, } ) path = plus_stations_path(project_root) if not rows and not path.exists(): return [] os.makedirs(path.parent, exist_ok=True) atomic_write_json(str(path), {"stations": rows}) return values def follow_plus_stations(project_root: str | Path, new_xy: Sequence[Sequence[float]]) -> int: """노선이 바뀔 때 좌표를 새 노선에 투영해 누가거리를 다시 매김 → 옮긴 수(같은 m 는 하나).""" rows = load_plus_stations(project_root) items = [ SimpleNamespace(chainage_m=float(r["chainage_m"]), x=r.get("x"), y=r.get("y")) for r in rows ] if not items: return 0 before = [item.chainage_m for item in items] follow_structures(items, new_xy) moved = sum(a != item.chainage_m for a, item in zip(before, items)) if moved: seen: set[float] = set() kept = [] for item in sorted(items, key=lambda i: i.chainage_m): if item.chainage_m in seen: continue seen.add(item.chainage_m) kept.append({"chainage_m": item.chainage_m, "x": item.x, "y": item.y}) atomic_write_json(str(plus_stations_path(project_root)), {"stations": kept}) return moved