Files
Aislo/resources/tester/test_349_0v_route_save_span.py
T
eomsangdonandClaude Opus 5.5 c95266efd0 fix(349-0v): 손으로 고친 노선 [저장] = 계획고 옮김만 · 바뀐 구간 = 화면과 같은 계획 폴리라인
손 편집 [저장]이 추천 갈래를 따라 균형 종단을 새로 풀어 사용자 계획고가 노선 전체에서 바뀌던 흠.
화면은 balanced_profile 안 보냄(profileShown 없앰) · 서버도 균형 종단은 as_initial(추천 [적용])일
때만. 계획고 옮김의 바뀐 구간 = 옛 계획 폴리라인(read_route_plan planned) 대 새로 쓴 계획
폴리라인 — 풀린 노선 geojson(점 간격 다름) 대신 화면 planline 단계와 같은 선. 거울 시험 test_349_0v.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-16 18:02:58 +09:00

130 lines
5.1 KiB
Python

# -*- coding: utf-8 -*-
"""349-0v 손으로 고친 노선 [저장] — 미리보기 계획고 = 저장 뒤 계획고(바뀐 구간 거울).
화면 planline 단계: 옛 = 저장 계획 폴리라인(`read_route_plan` planned) · 새 = `buildEditedPolyline`(노드
초안) → `changedSpan` → `carryProfileEdits`. 서버 노선 저장: 옛 = 같은 계획 폴리라인 CSV · 새 =
`_write_planned_polyline` 이 쓴 CSV → `changed_span` → `carry_profile_edits`. 두 길이 같은 구간 · 같은
계획고를 내야 함(풀린 노선 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_profile_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,
}
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"]]
)
base, edits = carry_profile_edits(world["base_pvi"], world["edits"], span, world["ground_pvi"])
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 반올림 · 화면 = 원값
assert edits == ts["edits"]
assert edits["station_offsets"]["150.000"] == 0.8 # 바뀐 구간 앞 사용자 계획고 그대로