fix(B05): 노선 원천을 한 곳으로 모으고 도엽 서피스를 기본 하나만 만든다

용화 자동 체인이 끝까지 돌았는데 종단 계획선이 PVI 3개 직선으로 나왔다. 관 22개
측점이 92.6~2044.9m 로 트림 전 원본(2,136m) 기준인데 확정 노선은 1,070.4m 라
21개가 노선 밖이었다. 좌표계도 갈려 있었다 — 관은 EPSG 5179(노선 파일), 노선은
5176(프로젝트)에 저장돼 누가거리만 우연히 맞물려 있었다.

원인은 계획노선을 읽는 곳이 흩어져 있는 것이다. 트림·조밀화를 체인 한 곳에만
넣었더니 배수유역·유입·도엽은 원본을 그대로 읽었다.

- load_design_route() 신규 — 읽기·좌표계 변환·트림·조밀화를 한 곳에서 끝낸다.
  설계 계통(체인·배수유역)은 전부 이 함수를 지난다.
- Router_Watershed 도 이 함수를 쓴다. 좌표계가 프로젝트 기준으로 통일돼 관과
  노선이 같은 공간에 놓인다.
- 도엽 서피스는 SHEET_SURFACE_AUTO_METHODS(기본 laplace 1종)만 자동 생성한다.
  여섯 방식을 매번 만들면 WF1 891초 중 655초를 여기서 쓴다. 나머지는 관리자가
  화면에서 고를 때 만든다.

Router_Inflow 는 노선을 EPSG 라벨용으로만 쓰고 기하를 안 써서 제외했다.
Engine_Extent(배경 지도 범위)도 제외 — 지도는 트림 전 전 구간을 덮어야 사용자가
측량 범위 밖을 볼 수 있다.

실측(용화 route 120):
  배수유역 노선   2,136m -> 1,070m   격자 889x1435 -> 545x831
  1차 영역        538,574㎡ -> 263,170㎡
  관 좌표계       EPSG 5179 -> 5176
  관              22개(10개가 노선 밖) -> 11개 전부 노선 안
  종단 PVI        3개 -> 13개, 종곡선 1 -> 11, 불균형 97.0% -> 86.2%
  PVI 측점이 관 측점과 일치한다(반올림 오차 제외).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 15:53:59 +09:00
co-authored by Claude Opus 5
parent e4b859766b
commit b014c4c1ec
7 changed files with 138 additions and 77 deletions
+13 -60
View File
@@ -28,70 +28,23 @@ logger = logging.getLogger(__name__)
def _planned_route_points_in_project_crs(
project_root: Path, surface: dict[str, Any] | None = None
) -> list[dict[str, float]] | None:
"""계획노선 파일(CSV·shapefile)을 읽어 프로젝트 좌표계(m) 점 목록으로 돌려준다.
"""설계용 계획노선을 프로젝트 좌표계(m) 점 목록으로 돌려준다. 없으면 None.
B04 `/planned-route` 조회와 같은 규칙 — 파일이 제 좌표계를 싣고 있고
프로젝트 좌표계와 다르면 한 번 옮긴다.
`surface`(확정 필터·방식·스무딩)를 받으면 지표면이 덮지 못하는 구간을 잘라 낸다.
라이다 측량이 노선 전 구간을 덮지 않는 현장이 있다(용화: 2,136m 중 1,400m).
자르지 않으면 체인이 서피스 밖 정점에서 끊긴다.
읽기·좌표계 변환·트림·조밀화는 `load_design_route()` 한 곳에서 한다 — 배수유역·유입도
같은 함수를 쓰므로 여기만 트림되는 일이 없다.
"""
from B04_PreProcess.B04_PreProcess_Engine_Extent import project_epsg_from_prj
from common_util.common_util_route_geometry import (
densify_route,
find_planned_route_file,
read_planned_route,
trim_route_to_surface,
)
from config.config_system import (
ROUTE_DIRECT_LINK_CELL_FACTOR,
ROUTE_GRID_RES_M,
ROUTE_PLANNED_DENSIFY_SAFETY,
)
from common_util.common_util_route_geometry import load_design_route
route_file = find_planned_route_file(project_root / "B03_FileInput" / "input")
planned = read_planned_route(route_file) if route_file else None
if planned is None or len(planned.vertices) < 2:
planned = load_design_route(project_root, surface)
if planned is None:
if surface:
logger.warning(
"자동 설계 체인 중단(설계 노선 없음): 계획노선을 읽지 못했거나 라이다 측량"
" 범위와 겹치지 않습니다 — %s",
project_root.name,
)
return None
target_epsg = project_epsg_from_prj(project_root)
points = [(float(vertex.x), float(vertex.y)) for vertex in planned.vertices]
source_epsg = planned.crs_input or target_epsg
if source_epsg.upper() != target_epsg.upper():
from pyproj import Transformer
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)
# 예정노선은 정점이 성기다(용화 평균 17.6m). 그대로 넘기면 구간마다 격자
# 탐색을 타고 급경사 현에서 끊긴다. 문턱 아래로 간격만 좁히면 계획노선이
# 손대지 않은 채 채택된다 — 같은 직선 위에 점을 더 찍는 것이라 형상 불변.
points = densify_route(
points,
ROUTE_DIRECT_LINK_CELL_FACTOR * ROUTE_GRID_RES_M * ROUTE_PLANNED_DENSIFY_SAFETY,
)
if len(points) < 2:
logger.warning(
"자동 설계 체인 중단(노선이 지표면 밖): 계획노선과 라이다 측량 범위가"
" 겹치지 않습니다 — %s",
project_root.name,
)
return None
return [{"x": x, "y": y} for x, y in points]
return [{"x": v.x, "y": v.y} for v in planned.vertices]
async def _prepare_drainage_pipes_and_reprofile(