- [확인] 이 고른 지형으로 확정 모델 · stage 1 params 를 바꾼 뒤 재계산 체인을 돎(switch_design_terrain · B04 확정 표시도 따라감) · 체인 실패면 노선과 함께 되돌림 - 수치지형도로 갈 때 라이다 확정값을 stage 1 params 에 적어 둠 — 라이다로 되돌려 [확인]하면 원래 모델 · params - 지형만 바꿔도 [확인]이 다시 셈 · 고르기 안 보이면(라이다 없는 프로젝트) 지형 안 실음 · 고른 값 없으면 지금 설계 지형 · 상태줄에 설계 지형 표시 - 분석 통로 terrain 기본 = 지금 설계 지형(None) · terrain-sources 가 design 을 함께 냄 - B06 구조물 측점 추가가 노선 확정 때 남긴 sampling.json 이 지금 지형과 다르면 stage 1 params 로 뜸(B04 재확정 때도 옛 지형으로 뜨던 흠) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
173 lines
7.1 KiB
Python
173 lines
7.1 KiB
Python
"""PLAN 133-1 — B05 계획노선 편집 분석 지형(라이다 · 수치지형도) 고르기.
|
|
|
|
라이다 있는 프로젝트 → 두 지형 다 고름 · sheet 고르면 dtm_sheet 로 표고 · 라이다 없는 프로젝트 →
|
|
고르기 안 보임(지형 하나) · 허용 목록 밖 거절 · 확정 params 는 안 바뀜.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from B05_Profile import B05_Profile_Router_Replan as replan
|
|
from B05_Profile import B05_Profile_Router_RouteInitial as initial
|
|
from B05_Profile import B05_Profile_Router_Terrain as terrain_router
|
|
from common_util.common_util_surface_confirmation import (
|
|
SHEET_ANALYSIS_FILTER,
|
|
analysis_terrain_params,
|
|
analysis_terrain_sources,
|
|
)
|
|
from common_util.common_util_surface_sampler import build_surface_sampler, check_surface_keys
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
LAS_PARAMS = {"source_filter": "csf", "method": "dtm", "smooth": True, "contour_interval_m": 1.0}
|
|
SHEET_PARAMS = {
|
|
"source_filter": SHEET_ANALYSIS_FILTER,
|
|
"method": "dtm",
|
|
"smooth": True,
|
|
"contour_interval_m": 1.0,
|
|
}
|
|
LAS_Z = 100.0
|
|
SHEET_Z = 250.0
|
|
|
|
|
|
def _dtm(path: Path, z: float) -> None:
|
|
x = np.arange(0.0, 11.0)
|
|
y = np.arange(0.0, 11.0)
|
|
np.savez(
|
|
path,
|
|
x=x,
|
|
y=y,
|
|
z=np.full((len(y), len(x)), z),
|
|
valid_mask=np.ones((len(y), len(x)), dtype=bool),
|
|
)
|
|
|
|
|
|
def _project(tmp_path: Path, *, las: bool, sheet: bool) -> Path:
|
|
models = tmp_path / "B04_PreProcess" / "models"
|
|
models.mkdir(parents=True)
|
|
if las:
|
|
_dtm(models / "dtm_csf_smooth.npz", LAS_Z)
|
|
if sheet: # B04 처럼 원본 · 평활 둘 다(평활 쪽을 읽음)
|
|
_dtm(models / f"dtm_{SHEET_ANALYSIS_FILTER}.npz", SHEET_Z - 1.0)
|
|
_dtm(models / f"dtm_{SHEET_ANALYSIS_FILTER}_smooth.npz", SHEET_Z)
|
|
return tmp_path
|
|
|
|
|
|
def _patch_db(monkeypatch, project_root: Path, params: dict) -> None:
|
|
class _Pool:
|
|
@asynccontextmanager
|
|
async def acquire(self):
|
|
yield object()
|
|
|
|
async def stored(_connection, _project_id):
|
|
return "x"
|
|
|
|
async def confirmed(_connection, _project_id):
|
|
return dict(params)
|
|
|
|
monkeypatch.setattr(terrain_router, "get_db_pool", lambda: _Pool())
|
|
monkeypatch.setattr(terrain_router, "get_project_storage_relative_path", stored)
|
|
monkeypatch.setattr(terrain_router, "get_surface_confirmation_params", confirmed)
|
|
monkeypatch.setattr(terrain_router, "resolve_stored_project_path", lambda _s: project_root)
|
|
|
|
|
|
PID = "1bb0f876-983f-4ca5-9cc7-f59ee1f77e0a"
|
|
|
|
|
|
def _elevations(terrain: str) -> dict:
|
|
request = terrain_router.ElevationRequest(points=[(5.0, 5.0)], terrain=terrain)
|
|
return asyncio.run(terrain_router.read_route_elevations(PID, request))
|
|
|
|
|
|
def test_라이다_있는_프로젝트_두_지형_다_고름(tmp_path, monkeypatch):
|
|
root = _project(tmp_path, las=True, sheet=True)
|
|
models = root / "B04_PreProcess" / "models"
|
|
assert analysis_terrain_sources(LAS_PARAMS, models) == ["las", "sheet"]
|
|
_patch_db(monkeypatch, root, LAS_PARAMS)
|
|
answer = asyncio.run(terrain_router.read_terrain_sources(PID))
|
|
assert answer["sources"] == ["las", "sheet"]
|
|
|
|
|
|
def test_sheet_고르면_dtm_sheet_로_표고(tmp_path, monkeypatch):
|
|
root = _project(tmp_path, las=True, sheet=True)
|
|
_patch_db(monkeypatch, root, LAS_PARAMS)
|
|
assert _elevations("las")["z"] == [LAS_Z]
|
|
assert _elevations("sheet")["z"] == [SHEET_Z]
|
|
# 기본값 = 지금 설계 지형(확정 지표면 · 133-2)
|
|
request = terrain_router.ElevationRequest(points=[(5.0, 5.0)])
|
|
assert request.terrain is None
|
|
|
|
|
|
def test_확정_params_는_안_바뀜():
|
|
params = dict(LAS_PARAMS)
|
|
picked = analysis_terrain_params(params, "sheet")
|
|
assert picked["source_filter"] == SHEET_ANALYSIS_FILTER == "sheet_laplace"
|
|
assert picked["method"] == "dtm" and picked["smooth"] is True
|
|
assert params == LAS_PARAMS
|
|
assert analysis_terrain_params(params, "las") is params
|
|
|
|
|
|
def test_라이다_없는_프로젝트_고르기_안_보임(tmp_path, monkeypatch):
|
|
root = _project(tmp_path, las=False, sheet=True)
|
|
models = root / "B04_PreProcess" / "models"
|
|
assert analysis_terrain_sources(SHEET_PARAMS, models) == ["sheet"]
|
|
# 확정이 이미 수치지형도 — 어느 값을 받아도 같은 서피스
|
|
assert analysis_terrain_params(SHEET_PARAMS, "sheet") is SHEET_PARAMS
|
|
_patch_db(monkeypatch, root, SHEET_PARAMS)
|
|
assert asyncio.run(terrain_router.read_terrain_sources(PID))["sources"] == ["sheet"]
|
|
# 허용 목록에 sheet_* 가 들어가 LAS 없는 프로젝트 지반고가 읽힘(54-22 뒤 막혀 있던 길)
|
|
assert _elevations("las")["z"] == [SHEET_Z]
|
|
|
|
|
|
def test_수치지형도_서피스_없으면_라이다만(tmp_path):
|
|
root = _project(tmp_path, las=True, sheet=False)
|
|
assert analysis_terrain_sources(LAS_PARAMS, root / "B04_PreProcess" / "models") == ["las"]
|
|
|
|
|
|
@pytest.mark.parametrize("bad", ["lidar", "SHEET", "../x", ""])
|
|
def test_허용_목록_밖_거절(bad):
|
|
with pytest.raises(ValidationError):
|
|
terrain_router.ElevationRequest(points=[(0.0, 0.0)], terrain=bad)
|
|
with pytest.raises(ValidationError):
|
|
terrain_router.CrossPreviewRequest(
|
|
vertices=[{"x": 0, "y": 0}, {"x": 1, "y": 1}], chainage_m=0, terrain=bad
|
|
)
|
|
with pytest.raises(ValidationError):
|
|
initial.ProfilePreviewRequest(vertices=[{"x": 0, "y": 0}, {"x": 1, "y": 1}], terrain=bad)
|
|
with pytest.raises(ValidationError):
|
|
replan.RouteInitialRequest(terrain=bad)
|
|
|
|
|
|
@pytest.mark.parametrize("bad", ["sheet", "sheet_../x", "sheet_LAPLACE", "dtm_sheet_laplace"])
|
|
def test_sheet_필터도_허용_목록_검사(bad, tmp_path):
|
|
with pytest.raises(ValueError):
|
|
check_surface_keys(bad)
|
|
with pytest.raises(ValueError):
|
|
build_surface_sampler(tmp_path, bad, "dtm", True)
|
|
check_surface_keys("sheet_laplace")
|
|
|
|
|
|
def test_화면_고르기_배선():
|
|
page = ROOT / "B05_Profile"
|
|
chrome = (page / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text(encoding="utf-8")
|
|
main = (page / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
|
api = (page / "B05_Profile_Api_Replan.ts").read_text(encoding="utf-8")
|
|
initial_api = (page / "B05_Profile_Api_RouteInitial.ts").read_text(encoding="utf-8")
|
|
# 기본 숨김 · 둘 다 있을 때만 보임 · 기본 라이다
|
|
assert '<label class="b05-routeedit__terrain" hidden' in chrome
|
|
assert '<option value="las">라이다</option>' in chrome
|
|
assert '<option value="sheet">수치지형도</option>' in chrome
|
|
assert 'sources?.sources.includes("las") && !!sources?.sources.includes("sheet")' in main
|
|
assert 'saved === "sheet" || saved === "las" ? saved : fallback' in api
|
|
# 모달 분석 넷이 고른 지형을 실어 보냄 · [확인] 재계산은 안 실음
|
|
assert api.count("terrain: analysisTerrain(projectId)") == 3
|
|
assert "terrain: analysisTerrain(projectId)" in initial_api
|
|
replan_fn = api.split("export async function replanRoute")[1].split("export ")[0]
|
|
assert "terrain" not in replan_fn
|