- 규칙: 구조물 · 관 기준 측점 = 정수 m 사사오입(일반 측점 1 m 붙기 뺌) · 측점 심는 자리 TS 짝(plantedChainages ↔ resolve_extra_stations) · 같은 m 면 구조물 측점 하나(격자 자리 grid) · 떠나면 일반 측점(oneStationList ↔ _merge_irregular_into_longitudinal) - 서버: B05 [저장] · B06 저장 · [측점 만들기]가 목록 전체 다시 맞춤(빈 목록도 · 옛 글 · 횡단 파일 글) · 계획선 측점 줄 다시 셈 · 정본에 없는 횡단 행은 0.5 m 안 새 측점으로 옮기고 나머지 지움 · 옛 소수 측점 꼭짓점 · 편집은 정수 자리로 · 맞춤 실패는 저장을 안 막음 - 화면: 종단 그래프 · 3D · 표 · ▲▼ 한 목록(초안 반영) · C군 구간형 · 독립 기슭막이 시작 · 끝 측점 · B06 카드는 초안에 없는 옛 구조물 측점 숨김 · 고름은 같은 m 구조물 측점 먼저 · 폼 거리 칸 정수 표기 · C군 계산 구간 · 포장 나눔 경계 정수 - 옛 자료: [정수 맞춤] 단추 없앰 · 종단 보기 열 때 한 번 자동 정수 · 알림 한 줄 · [저장]하면 남음 - 시험: test_272_1 · test_272_2 새로 · 234_1 · 238_1 · b05_extra_stations_supplier · b06_patch_station · b05_station_roundtrip · b05_b06_save_roundtrip 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
194 lines
8.6 KiB
Python
194 lines
8.6 KiB
Python
"""272-2 — 구조물 측점을 지금 목록 전체로 다시 맞춤(요구 167 · 측점 겹침 근본).
|
|
|
|
서버는 관 · 구조물 목록 **전체**로 종단 정본의 구조물 측점을 다시 맞춘다 — 빈 목록이어도 옛 구조물
|
|
측점을 지우고, 구조물이 떠난 격자 자리는 일반 측점으로 되돌리며(횡단 파일 글까지), 같은 m 면
|
|
측점 하나(구조물이 위 · `grid`). 종단 정본에 없는 측점의 횡단 행도 지운다(B08 이 옛 자리를 안 셈).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import math
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from B05_Profile.B05_Profile_Engine_Sections import cross_filename # noqa: E402
|
|
from B05_Profile.B05_Profile_Repository import sync_cross_section_rows # noqa: E402
|
|
from B05_Profile.B05_Profile_Router_Confirm import _merge_irregular_into_longitudinal # noqa: E402
|
|
|
|
REL = "B06_Section/longitudinal/longitudinal.json"
|
|
PIPE = "파형강관 D800"
|
|
|
|
|
|
def _write(path: Path, data: dict) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
|
|
|
|
|
def _cross(root: Path, chainage: float) -> Path:
|
|
return root / "B06_Section/cross_sections" / cross_filename(chainage)
|
|
|
|
|
|
def _setup(root: Path) -> None:
|
|
"""격자 0~100(20 m) · 옛 관 측점 95.38 · 옛 판 「regular + 글」 60 m."""
|
|
grid = [
|
|
{"chainage_m": float(c), "kind": "bp" if c == 0 else "regular"} for c in range(0, 101, 20)
|
|
]
|
|
grid[3]["structure"] = "옛 글"
|
|
stations = sorted(
|
|
[*grid, {"chainage_m": 95.38, "kind": "irregular", "structure": PIPE}],
|
|
key=lambda s: s["chainage_m"],
|
|
)
|
|
_write(root / REL, {"stations": stations})
|
|
for station in stations:
|
|
_write(_cross(root, station["chainage_m"]), {**station, "samples": []})
|
|
|
|
|
|
def _stations(root: Path) -> list[dict]:
|
|
return json.loads((root / REL).read_text(encoding="utf-8"))["stations"]
|
|
|
|
|
|
def _at(root: Path, chainage: float) -> dict:
|
|
return next(s for s in _stations(root) if s["chainage_m"] == chainage)
|
|
|
|
|
|
def test_관이_격자_위로_가면_측점_하나_구조물이_위(tmp_path: Path) -> None:
|
|
_setup(tmp_path)
|
|
moved = {"chainage_m": 100.0, "kind": "irregular", "grid": True, "structure": PIPE}
|
|
_merge_irregular_into_longitudinal(tmp_path, REL, [moved])
|
|
chainages = [s["chainage_m"] for s in _stations(tmp_path)]
|
|
assert 95.38 not in chainages and chainages.count(100.0) == 1
|
|
assert _at(tmp_path, 100.0) == moved
|
|
# 옛 판 「regular + 글」 도 글을 지움 — 횡단 파일 글까지
|
|
assert _at(tmp_path, 60.0) == {"chainage_m": 60.0, "kind": "regular"}
|
|
cross = json.loads(_cross(tmp_path, 60.0).read_text(encoding="utf-8"))
|
|
assert cross["kind"] == "regular" and "structure" not in cross
|
|
|
|
|
|
def test_구조물이_떠나면_일반_측점이_돌아옴_빈_목록도(tmp_path: Path) -> None:
|
|
_setup(tmp_path)
|
|
on_grid = {"chainage_m": 100.0, "kind": "irregular", "grid": True, "structure": PIPE}
|
|
_merge_irregular_into_longitudinal(tmp_path, REL, [on_grid])
|
|
_write(_cross(tmp_path, 100.0), {**on_grid, "samples": []}) # 측점 만들기가 쓴 파일 흉내
|
|
_merge_irregular_into_longitudinal(tmp_path, REL, [])
|
|
stations = _stations(tmp_path)
|
|
assert [s["chainage_m"] for s in stations] == [0.0, 20.0, 40.0, 60.0, 80.0, 100.0]
|
|
assert all(s["kind"] in ("bp", "regular") and "structure" not in s for s in stations)
|
|
assert all("grid" not in s for s in stations)
|
|
cross = json.loads(_cross(tmp_path, 100.0).read_text(encoding="utf-8"))
|
|
assert cross["kind"] == "regular" and "structure" not in cross and "grid" not in cross
|
|
|
|
|
|
def test_시점_위_구조물이_떠나도_시점은_시점(tmp_path: Path) -> None:
|
|
_setup(tmp_path)
|
|
at_bp = {"chainage_m": 0.0, "kind": "bp", "grid": True, "structure": PIPE}
|
|
_merge_irregular_into_longitudinal(tmp_path, REL, [at_bp])
|
|
assert _at(tmp_path, 0.0) == at_bp
|
|
_merge_irregular_into_longitudinal(tmp_path, REL, [])
|
|
assert _at(tmp_path, 0.0) == {"chainage_m": 0.0, "kind": "bp"}
|
|
|
|
|
|
def test_측점만_옮겨도_계획선_측점_줄을_다시_셈(tmp_path: Path) -> None:
|
|
"""편집 없는 관 측점 581.9 → 582 — 계획선 측점 줄(표 · ▲▼)이 옛 측점을 들고 남지 않음."""
|
|
source = PROJECT_ROOT / "resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json"
|
|
path = tmp_path / REL
|
|
_write(path, json.loads(source.read_text(encoding="utf-8")))
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
moved = [
|
|
{**s, "chainage_m": 582.0} if s["chainage_m"] == 581.9 else s
|
|
for s in data["stations"]
|
|
if s.get("kind") == "irregular"
|
|
]
|
|
assert _merge_irregular_into_longitudinal(tmp_path, REL, moved) is not None
|
|
rows = [row["chainage_m"] for row in _stations_rows(path)]
|
|
assert 582.0 in rows and 581.9 not in rows
|
|
assert rows == [s["chainage_m"] for s in _stations(tmp_path)]
|
|
# 그대로 한 번 더 — 줄이 측점과 같으면 안 건드림
|
|
assert _merge_irregular_into_longitudinal(tmp_path, REL, moved) is None
|
|
|
|
|
|
def test_옛_소수_측점을_정수로_옮기면_꼭짓점_편집도_따라감(tmp_path: Path) -> None:
|
|
"""484.213 · 581.902 · 676.591 · 769.28 → 정수 m — 꼭짓점이 높이 그대로 옮겨 감(지우면 계획선이
|
|
그 자리에서 바뀜 · 남기면 측점 없는 꼭짓점이 1000% 넘게 꺾음 · 브라우저 실측)."""
|
|
source = PROJECT_ROOT / "resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json"
|
|
data = json.loads(source.read_text(encoding="utf-8"))
|
|
_write(tmp_path / REL, data)
|
|
snapped = [
|
|
{**s, "chainage_m": float(math.floor(s["chainage_m"] + 0.5))}
|
|
for s in data["stations"]
|
|
if s.get("kind") == "irregular"
|
|
]
|
|
_merge_irregular_into_longitudinal(tmp_path, REL, snapped)
|
|
after = json.loads((tmp_path / REL).read_text(encoding="utf-8"))["profile_alignment"]
|
|
nodes = {n["chainage_m"]: n["elevation_m"] for n in after["base_pvi"]}
|
|
old_nodes = {n["chainage_m"]: n["elevation_m"] for n in data["profile_alignment"]["base_pvi"]}
|
|
for old, new in ((484.213, 484.0), (581.902, 582.0), (676.591, 677.0), (769.28, 769.0)):
|
|
assert old not in nodes and nodes[new] == old_nodes[old]
|
|
offsets = after["edits"]["station_offsets"]
|
|
assert "769.280" not in offsets and "769.000" in offsets
|
|
|
|
|
|
def _stations_rows(path: Path) -> list[dict]:
|
|
return json.loads(path.read_text(encoding="utf-8"))["profile_alignment"]["stations"]
|
|
|
|
|
|
class _Cursor:
|
|
def __init__(self, rows: list[tuple[int, float]]) -> None:
|
|
self.rows = rows
|
|
self.moved: list[tuple[float, int]] = []
|
|
self.deleted: list[int] | None = None
|
|
|
|
async def __aenter__(self) -> "_Cursor":
|
|
return self
|
|
|
|
async def __aexit__(self, *_a) -> bool:
|
|
return False
|
|
|
|
async def execute(self, sql: str, args=None) -> None:
|
|
if sql.startswith("UPDATE"):
|
|
self.moved.append(tuple(args))
|
|
elif sql.startswith("DELETE"):
|
|
self.deleted = list(args)
|
|
|
|
async def fetchall(self) -> list[tuple[int, float]]:
|
|
return self.rows
|
|
|
|
|
|
class _Connection:
|
|
def __init__(self, rows: list[tuple[int, float]]) -> None:
|
|
self.cursor_ = _Cursor(rows)
|
|
|
|
def cursor(self) -> _Cursor:
|
|
return self.cursor_
|
|
|
|
|
|
def _sync(rows: list[tuple[int, float]], stations: list[float]) -> tuple[int, _Cursor]:
|
|
connection = _Connection(rows)
|
|
return asyncio.run(sync_cross_section_rows(connection, 7, stations)), connection.cursor_
|
|
|
|
|
|
def test_옛_소수_측점_행은_새_정수_측점으로_옮김() -> None:
|
|
"""95.38 → 95 — 사용자 설계 값이 든 행을 그대로 옮김(면적은 저장 뒤 서버 재계산)."""
|
|
count, cursor = _sync([(1, 0.0), (2, 95.38), (3, 100.004)], [0.0, 20.0, 95.0, 100.0])
|
|
assert count == 1 and cursor.moved == [(95.0, 2)] and cursor.deleted is None
|
|
|
|
|
|
def test_가까운_새_측점이_없으면_지움() -> None:
|
|
count, cursor = _sync([(1, 0.0), (2, 95.38), (5, 140.0)], [0.0, 20.0, 100.0])
|
|
assert count == 2 and cursor.moved == [] and cursor.deleted == [2, 5]
|
|
|
|
|
|
def test_한_측점에_둘이_다투면_가까운_행만_옮김() -> None:
|
|
count, cursor = _sync([(2, 95.38), (6, 94.7), (7, 100.0)], [95.0, 100.0])
|
|
assert count == 2 and cursor.moved == [(95.0, 6)] and cursor.deleted == [2]
|
|
|
|
|
|
def test_측점_목록이_비면_아무것도_안_건드림() -> None:
|
|
count, cursor = _sync([(1, 0.0), (2, 95.38)], [])
|
|
assert count == 0 and cursor.moved == [] and cursor.deleted is None
|