Files
Aislo/resources/tester/test_349_0v_route_save_span.py
T
eomsangdonandClaude Opus 5.5 a1097653e7 feat(B05 종단 이월): 평면 고친 뒤 같은 자리(옆 1 m · 방향 30° 안) 종단 편집 이어받음 — 새 측점 위에 옛 계획고 · 바뀐 자리만 새 지반 첫 계획선 · 기준 한 벌에서 한 번(걸음 수 무관 · 서버 짝) (400)
- 같은 자리 = 새 선 측점을 옛 선에 투영 · 측점 스냅이 뒤 교점을 밀어도 이어받음(옛 = 1 mm 정점 비교라 뒤쪽 전부 다시)
- 자동 꼭짓점 · 편집(직선화 · 자유지정 · 쉬프트 · ▲▼ · R)을 새 측점 위에 다시 놓음 — [저장] 측점 밖 키 버림에 안 걸림
- follow 기준 = 마지막 종단 편집 때 선 · 꼭짓점 · 편집 한 벌 · 되돌린 노선 = 옛 편집
- 짝 = B05_Profile_Engine_Grade_Carry.py ↔ logic/B05_Profile_Grade_Carry.ts · 거울 시험 test_400_profile_carry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
2026-10-17 21:53:38 +09:00

140 lines
5.6 KiB
Python

# -*- coding: utf-8 -*-
"""349-0v 손으로 고친 노선 [저장] — 미리보기 계획고 = 저장 뒤 계획고(바뀐 구간 거울).
화면 planline 단계: 옛 = 저장 계획 폴리라인(`read_route_plan` planned) · 새 = `buildEditedPolyline`(노드
초안) → `changedSpan` → `carryBase` · `carryEdits`. 서버 노선 저장: 옛 = 같은 계획 폴리라인
CSV · 새 = `_write_planned_polyline` 이 쓴 CSV → `changed_span` → `carry_base` · `carry_edits`(400).
두 길이 같은 구간 · 같은 계획고를 내야 함(풀린 노선 geojson 으로 가르면 점 간격이 달라
앞머리가 일찍 갈렸음).
"""
from __future__ import annotations
import json
import math
import subprocess
import sys
from pathlib import Path
import pytest
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B05_Profile import B05_Profile_Router_Replan as replan # noqa: E402
from B05_Profile.B05_Profile_Engine_Grade_Carry import ( # noqa: E402
carry_base,
carry_edits,
changed_span,
)
pytestmark = pytest.mark.skipif(
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
reason="프론트엔드 의존성 미설치",
)
RADIUS_M = 30.0
OLD_NODES = [(0.0, 0.0), (180.0, 20.0), (320.0, 140.0), (480.0, 130.0), (620.0, 260.0)]
OLD_NODES += [(800.0, 250.0), (950.0, 380.0)]
MOVED = 4 # 가운데 뒤쪽 꺾임점 하나를 끎(검증_sub2c 끌기와 같은 꼴)
NEW_NODES = [(x + 25.0, y - 30.0) if i == MOVED else (x, y) for i, (x, y) in enumerate(OLD_NODES)]
CURVE_ON = [True] * len(OLD_NODES)
RADII: list[float | None] = [None] * len(OLD_NODES)
def _csv_line(path: Path, nodes) -> list[list[float]]:
replan._write_planned_polyline(
path, list(nodes), RADIUS_M, simplify=False, curve_flags=CURVE_ON, radii=RADII
)
return replan._vertices_of(path)
def _length(line) -> float:
return sum(math.dist(a, b) for a, b in zip(line, line[1:]))
@pytest.fixture(scope="module")
def world(tmp_path_factory):
tmp = tmp_path_factory.mktemp("span")
old_line = _csv_line(tmp / "old.csv", OLD_NODES) # 저장 노선(화면 · 서버 같은 파일)
new_line = _csv_line(tmp / "new.csv", NEW_NODES) # 서버가 [저장] 때 쓰는 새 계획 폴리라인
total_old, total_new = _length(old_line), _length(new_line)
base_pvi = [
{"chainage_m": round(s, 6), "elevation_m": 100.0 + 0.04 * s}
for s in [0.0, 150.0, 300.0, 450.0, 600.0, 750.0, 900.0, total_old]
]
ground_pvi = [
{"chainage_m": round(s, 6), "elevation_m": 101.0 + 0.03 * s}
for s in [x * 40.0 for x in range(int(total_new // 40) + 1)] + [total_new]
]
edits = {
"station_offsets": {"150.000": 0.8, "600.000": -0.5, "900.000": 0.4},
"curve_radii": {"300.000": 400.0},
}
payload = {
"nodes": [list(p) for p in NEW_NODES],
"curveOn": CURVE_ON,
"curveRadius": RADII,
"minRadiusM": RADIUS_M,
"old_line": old_line,
"base_pvi": base_pvi,
"edits": edits,
"ground_pvi": ground_pvi,
"stations": [p["chainage_m"] for p in ground_pvi],
}
source = tmp / "input.json"
source.write_text(json.dumps(payload), encoding="utf-8")
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HERE / "helper_349_0v_span.cjs"), str(source)],
capture_output=True,
text=True,
encoding="utf-8",
check=True,
)
return {**payload, "new_line_py": new_line, "ts": json.loads(proc.stdout)}
def test_새_계획_폴리라인_화면_서버_같음(world):
ts_line = world["ts"]["new_line"]
assert len(ts_line) == len(world["new_line_py"])
assert max(math.dist(a, b) for a, b in zip(ts_line, world["new_line_py"])) < 1e-3
def test_바뀐_구간_화면_서버_같음(world):
span = changed_span(
[tuple(p) for p in world["old_line"]], [tuple(p) for p in world["new_line_py"]]
)
ts_span = world["ts"]["span"]
assert span is not None and ts_span is not None
for key in ("start_m", "old_end_m", "new_end_m", "delta_m"):
assert span[key] == pytest.approx(ts_span[key], abs=1e-3), key
assert (span["start_fixed"], span["end_fixed"]) == (
ts_span["start_fixed"],
ts_span["end_fixed"],
)
# 끈 꺾임점 앞앞 꺾임점의 곡선 끝(마지막 같은 점)부터 — 노선 앞머리부터 다시 풀지 않음
assert _length(OLD_NODES[: MOVED - 1]) < span["start_m"] < _length(OLD_NODES[:MOVED])
def test_옮긴_계획고_화면_서버_같음(world):
span = changed_span(
[tuple(p) for p in world["old_line"]], [tuple(p) for p in world["new_line_py"]]
)
old, new = world["old_line"], world["new_line_py"]
base = carry_base(old, new, world["base_pvi"], world["ground_pvi"])
edits = carry_edits(old, new, world["base_pvi"], world["edits"], world["stations"], 20.0)
assert span is not None
ts = world["ts"]["result"]
flat = [v for n in base for v in (n["chainage_m"], n["elevation_m"])]
assert flat == pytest.approx(
[v for n in ts["basePvi"] for v in (n["chainage_m"], n["elevation_m"])], abs=1e-3
) # 서버 새 선 = CSV 0.1 mm 반올림 · 화면 = 원값
for field in ("station_offsets", "curve_radii"):
assert edits[field].keys() == ts["edits"][field].keys()
for key, value in edits[field].items():
assert value == pytest.approx(ts["edits"][field][key], abs=1e-3)
# 바뀐 구간 앞 사용자 계획고(150) = 둘레 새 측점(120 · 160) 편집으로 그대로
assert {"120.000", "160.000"} <= set(edits["station_offsets"])