"""383-1 R = 일반측점 기본 제약 — 새 초기 노선 맞춤 + [측점 스냅] 켬 · 추천 후보 맞춤 · [적용] 켬. ./venv/Scripts/python.exe -m pytest resources/tester/test_383_1_route_station_default.py -q 새 사본(예상노선만) → 초기 노선 곡선 MC 가 일반측점 위(≈ 0 밖) · 곡선표 `station_snap` 켬 · 추천 「예상노선 추적」(켬) 노드 = 초기 노선 노드 · 측점 밖 곡선 = 「못 지킨 구간」(station) · 옛 사본(초기 노선 이미 있음)은 파일 · 깃발 그대로(말없이 측점 안 바뀜) · 추천 [적용] 은 스냅 켬. 첫 셈(검증_sub1 · 14 % · R12 · 묶음 + 영선) 74.6 s → 42.6 s(10-17 실측 · 같은 기계). """ import json import shutil from pathlib import Path import pytest from shapely.geometry import LineString, Point from B05_Profile.B05_Profile_Engine_RouteInitial import generate_initial_route from B05_Profile.B05_Profile_Engine_RouteInitial_Station import MC_TOL_M, STATION_M from B05_Profile.B05_Profile_Engine_Sections import load_station_snap from B05_Profile.B05_Profile_Router_Replan import ( _curves_path, _ensure_planned_initial, _nodes_path, _write_planned_polyline, ) from common_util.common_util_route_geometry import ( expected_route_csv_path, planned_route_initial_path, read_planned_route_csv, ) ROOT = Path(__file__).resolve().parents[2] PROJECT = ROOT / "storage/1/3/7e420ed6-f940-4d12-a1a5-39cdcf70a391" # 검증_sub1 RADIUS = 12.0 def _copy_expected(tmp_path: Path) -> Path: source = expected_route_csv_path(PROJECT) if not source.is_file(): pytest.skip("검증_sub1 storage 없음") target = expected_route_csv_path(tmp_path) target.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(source, target) return tmp_path def _off_station(path: Path) -> tuple[int, int]: """(측점 밖 곡선 수, 곡선 수) — 저장 파일에서 다시 잼(MC = BC · EC 누가거리 가운데).""" line = LineString([(v.x, v.y) for v in read_planned_route_csv(path).vertices]) curves = json.loads(_curves_path(path).read_text(encoding="utf-8"))["curves"] taken, off = set(), 0 for curve in curves: mc = (line.project(Point(curve["start"])) + line.project(Point(curve["end"]))) / 2 n = round(mc / STATION_M) if n >= 1 and abs(mc - n * STATION_M) <= MC_TOL_M and n not in taken: taken.add(n) else: off += 1 return off, len(curves) def test_new_copy_initial_on_stations_and_flag_on(tmp_path: Path) -> None: root = _copy_expected(tmp_path) assert _ensure_planned_initial(root, RADIUS) is not None initial = planned_route_initial_path(root) off, total = _off_station(initial) print(f"새 초기 노선 곡선 {total} · 측점 밖 {off}") assert total and off <= 1 assert load_station_snap(root) is True def test_follow_nodes_equal_initial(tmp_path: Path) -> None: """추천 「예상노선 추적」(측점 맞춤 켬) = 새 초기 노선 · 측점 밖 곡선 수 = station 목록 수.""" surface = PROJECT / "B05_Profile/route/cost_surface_classification_dtm_smooth.npz" if not surface.is_file(): pytest.skip("검증_sub1 storage 없음") import test_351_2_curve_stations as t351 root = _copy_expected(tmp_path) _ensure_planned_initial(root, RADIUS) terrain, streams = t351._project_terrain() expected = [(v.x, v.y) for v in read_planned_route_csv(expected_route_csv_path(root)).vertices] result = generate_initial_route( expected, terrain, streams, mode="follow", weights=None, max_grade=0.14, min_radius_m=RADIUS, with_profile=False, ) saved = [ (v.x, v.y) for v in read_planned_route_csv(_nodes_path(planned_route_initial_path(root))).vertices ] nodes = [(node["x"], node["y"]) for node in result["nodes"]] assert len(nodes) == len(saved) assert all(abs(a[0] - b[0]) <= 1e-3 and abs(a[1] - b[1]) <= 1e-3 for a, b in zip(nodes, saved)) misses = [v for v in result["metrics"]["violations"] if v["kind"] == "station"] assert len(misses) == result["metrics"]["over_limit"]["station"] == result["off_station_curves"] assert result["unsnapped"] is not None # 「측점 맞춤」 끔 = 맞추기 전 선 def test_old_copy_flag_and_line_unchanged(tmp_path: Path) -> None: """옛 사본 — 초기 노선(깃발 칸 없음)이 이미 있으면 다시 안 씀 · 스냅 끔 그대로.""" root = _copy_expected(tmp_path) initial = planned_route_initial_path(root) points = [(v.x, v.y) for v in read_planned_route_csv(expected_route_csv_path(root)).vertices] _write_planned_polyline(initial, points, RADIUS) # 옛 규칙(맞춤 없음 · 칸 없음) before = {p: p.read_bytes() for p in (initial, _curves_path(initial), _nodes_path(initial))} assert _ensure_planned_initial(root, RADIUS) is None assert {p: p.read_bytes() for p in before} == before assert load_station_snap(root) is False def test_apply_saves_station_snap_on() -> None: text = (ROOT / "B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8") assert "routeDraftOf(nodes, { ...settings, stationSnap: true, stationSnapStep: null })" in text