- 같은 자리 = 새 선 측점을 옛 선에 투영 · 측점 스냅이 뒤 교점을 밀어도 이어받음(옛 = 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
140 lines
5.6 KiB
Python
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"])
|