feat(B04): 계획노선을 3D 최고 표고 평면에 그리고, 지표면 밖 구간은 잘라 낸다
실무 자료 용화.las가 계획노선 2,136m 중 일부만 덮는다. 좌표 문제가 아니라 측량 범위 자체다 — LAS(VLR EPSG 5176)와 정사영상 용화.tif 범위가 서로 일치하고, 위경도로 datum 보정까지 태워도 위도는 완전히 포함되며 경도만 동쪽 431m 초과한다. 노선 3D 표시 - 계획노선을 지표면에 드리우지 않고 데이터 최고 표고(bounds.z_max) 평면에 수평으로 얹는다. 노선과 측량 범위가 평면상 어디서 어긋나는지 보려는 것이라 지형을 따라 오르내리면 오히려 판단이 어렵다. - 색은 2D 지도·B05 배수유역도가 쓰는 routeLineColor()를 그대로 쓴다. 같은 선을 두 화면에서 다른 색으로 그리면 같은 것인지 알아볼 수 없다. 노선 트림 - trim_route_to_surface(): DtmGridSampler 의 valid_mask 로 판정한다. bounds 사각형이 아니라 불규칙한 실제 외곽이다. 가장 긴 연속 유효 구간을 남긴다. - 가장자리 여유 SURFACE_ROUTE_EDGE_TRIM_M(30m)은 잘라 낸 쪽 끝에만 적용한다. 노선 본래 끝점이 지표면 안이면 깎지 않는다. - 자르는 자리는 _planned_route_points_in_project_crs() 한 곳이다. 체인의 BP·EP·CP가 전부 이 함수를 지나므로 여기서 한 번 자르면 하류가 모두 유효해진다. - 지표면을 못 열면 자르지 않는다. 트림 실패가 설계를 막으면 안 된다. 실측(용화 노선 2,136m): csf/dtm/smooth -> 1,310m (61%) classification/dtm/smooth -> 1,070m (50%) bounds 사각형 기준 추정치 1,400m보다 짧다 — 실제 외곽이 사각형보다 작기 때문이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,16 +25,23 @@ from fastapi.responses import JSONResponse
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _planned_route_points_in_project_crs(project_root: Path) -> list[dict[str, float]] | None:
|
||||
def _planned_route_points_in_project_crs(
|
||||
project_root: Path, surface: dict[str, Any] | None = None
|
||||
) -> list[dict[str, float]] | None:
|
||||
"""계획노선 CSV를 읽어 프로젝트 좌표계(m) 점 목록으로 돌려준다. 없으면 None.
|
||||
|
||||
B04 `/planned-route` 조회와 같은 규칙 — CSV가 제 좌표계(crs_epsg)를 적어 두었고
|
||||
프로젝트 좌표계와 다르면 한 번 옮긴다.
|
||||
|
||||
`surface`(확정 필터·방식·스무딩)를 받으면 지표면이 덮지 못하는 구간을 잘라 낸다.
|
||||
라이다 측량이 노선 전 구간을 덮지 않는 현장이 있다(용화: 2,136m 중 1,400m).
|
||||
자르지 않으면 체인이 서피스 밖 정점에서 끊긴다.
|
||||
"""
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Extent import project_epsg_from_prj
|
||||
from common_util.common_util_route_geometry import (
|
||||
find_planned_route_file,
|
||||
read_planned_route_csv,
|
||||
trim_route_to_surface,
|
||||
)
|
||||
|
||||
route_file = find_planned_route_file(project_root / "B03_FileInput" / "input")
|
||||
@@ -49,6 +56,28 @@ def _planned_route_points_in_project_crs(project_root: Path) -> list[dict[str, f
|
||||
|
||||
transformer = Transformer.from_crs(source_epsg, target_epsg, always_xy=True)
|
||||
points = [transformer.transform(x, y) for x, y in points]
|
||||
|
||||
if surface:
|
||||
from common_util.common_util_surface_sampler import build_surface_sampler
|
||||
|
||||
try:
|
||||
sampler = build_surface_sampler(
|
||||
project_root / "B04_PreProcess" / "models",
|
||||
str(surface["source_filter"]),
|
||||
str(surface["method"]),
|
||||
bool(surface["smooth"]),
|
||||
)
|
||||
except (FileNotFoundError, KeyError, OSError) as exc:
|
||||
logger.warning("자동 설계 체인 노선 트림 건너뜀 — 지표면을 열지 못했습니다: %s", exc)
|
||||
else:
|
||||
points = trim_route_to_surface(points, sampler)
|
||||
if len(points) < 2:
|
||||
logger.warning(
|
||||
"자동 설계 체인 중단(노선이 지표면 밖): 계획노선과 라이다 측량 범위가"
|
||||
" 겹치지 않습니다 — %s",
|
||||
project_root.name,
|
||||
)
|
||||
return None
|
||||
return [{"x": x, "y": y} for x, y in points]
|
||||
|
||||
|
||||
@@ -172,16 +201,18 @@ async def run_auto_design_chain(
|
||||
# 이 체인이 끝나기 전에는 B05·B06에 들어가면 안 된다 — 반쯤 계산된 화면을 만지면
|
||||
# 그 편집이 섞인 채 초기값이 찍힌다(2026-08-29 사용자 확정, CLAUDE.md 5장).
|
||||
mark_designing(project_root)
|
||||
points = _planned_route_points_in_project_crs(project_root)
|
||||
# WF1이 **실제로 확정한** 선택값(stage 1 스냅샷)을 그대로 쓴다. config 기본값을
|
||||
# 쓰면 LAS 없이 설계한 프로젝트에서 있지도 않은 모델을 가리켜 404로 체인이
|
||||
# 끊긴다(2026-08-30 실사고). 노선 트림도 이 지표면을 기준으로 한다.
|
||||
async with pool.acquire() as connection:
|
||||
defaults = await get_surface_confirmation_params(connection, str(project_id))
|
||||
|
||||
points = _planned_route_points_in_project_crs(project_root, defaults)
|
||||
if not points:
|
||||
logger.warning("자동 설계 체인 중단(계획노선 CSV 없음): project_id=%s", project_id)
|
||||
return None
|
||||
|
||||
# 3) B05 경로 계산 — WF1이 **실제로 확정한** 선택값(stage 1 스냅샷)을 그대로 쓴다.
|
||||
# config 기본값(csf/dtm)을 쓰면 LAS 없이 설계한 프로젝트에서 있지도 않은 모델을
|
||||
# 가리켜 404로 체인이 끊긴다(2026-08-30 실사고).
|
||||
async with pool.acquire() as connection:
|
||||
defaults = await get_surface_confirmation_params(connection, str(project_id))
|
||||
# 3) B05 경로 계산
|
||||
request = RouteSolveRequest(
|
||||
filter_key=str(defaults["source_filter"]),
|
||||
method=str(defaults["method"]),
|
||||
|
||||
Reference in New Issue
Block a user