diff --git a/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py b/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py index dd52ea62..cd6646ad 100644 --- a/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py +++ b/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py @@ -56,7 +56,7 @@ SHEET_SOURCE_FILTER = "sheet" def _load_features_metric( - processed_dir: Path, epsg: int, filename: str = _CONTOUR_FILE + processed_dir: Path, crs: str, filename: str = _CONTOUR_FILE ) -> list[dict[str, Any]]: """병합 도엽 레이어(WGS84)를 읽어 사업지 CRS(m)로 재투영한다.""" path = processed_dir / filename @@ -72,7 +72,7 @@ def _load_features_metric( features = data.get("features") if not isinstance(features, list): return [] - transformer = Transformer.from_crs("EPSG:4326", f"EPSG:{epsg}", always_xy=True) + transformer = Transformer.from_crs("EPSG:4326", crs, always_xy=True) def _map(coords: Any) -> Any: if not isinstance(coords, list): @@ -381,7 +381,7 @@ def build_sheet_surface_model( processed_dir: Path, models_dir: Path, route_xy: np.ndarray, - epsg: int, + crs: str, methods: list[str] | None = None, ) -> list[dict[str, Any]]: """도엽등고선으로 방식별 DTM npz·프리뷰 glb를 만들고 등록용 dict 목록을 돌려준다. @@ -390,10 +390,10 @@ def build_sheet_surface_model( 화면에서 바꿔 보며 정해야 한다(2026-08-30 사용자 지시). 실패하면 빈 목록 — 호출측은 분석을 계속한다(도엽 미확보 지역 폴백). - `route_xy`: (N, 2) 노선 정점 XY(사업지 CRS, m). + `route_xy`: (N, 2) 노선 정점 XY(사업지 CRS, m), `crs`: 그 좌표계(pyproj 입력 문자열). """ started = time.monotonic() - features = _load_features_metric(processed_dir, epsg) + features = _load_features_metric(processed_dir, crs) if not features: return [] @@ -456,22 +456,20 @@ def build_sheet_surface_from_route( `methods`를 주지 않으면 `SHEET_SURFACE_METHODS` 전체를 만든다 — 관리자가 화면에서 한 방식을 요청할 때 그 목록만 넘긴다. """ - from common_util.common_util_route_geometry import ( - find_planned_route_file, - read_planned_route, - ) + from B04_PreProcess.B04_PreProcess_Engine_Extent import project_epsg_from_prj + from common_util.common_util_route_geometry import load_design_route - route_file = find_planned_route_file(project_root / "B03_FileInput" / "input") - if route_file is None: - logger.warning("도엽 서피스: 계획 노선 파일이 없습니다.") - return [] - planned = read_planned_route(route_file) + # 노선 CSV의 `crs_epsg` 열은 표시용 라벨이라 LAS(.prj) 좌표계와 다를 수 있다 + # (2026-09-01 실측: 라벨 5179, 실제 5176 — 도엽 서피스만 딴 자리에 만들어졌다). + # `load_design_route()`가 .prj 좌표계로 재투영해 주므로 라이다 지표면과 한 자리에 선다. + planned = load_design_route(project_root) if planned is None or len(planned.vertices) < 2: - logger.warning("도엽 서피스: 계획 노선 파일을 읽지 못했습니다: %s", route_file.name) + logger.warning("도엽 서피스: 계획 노선 파일을 읽지 못했습니다.") return [] + crs = planned.crs_input or project_epsg_from_prj(project_root) route_xy = np.array([(v.x, v.y) for v in planned.vertices], dtype=np.float64) return build_sheet_surface_model( - project_root, processed_dir, models_dir, route_xy, planned.epsg or 5186, methods + project_root, processed_dir, models_dir, route_xy, crs, methods ) @@ -527,7 +525,9 @@ def run_sheet_surface_analysis( ) _report(70, "surface_model", "도엽등고선 3D 서피스 생성 중") - models = build_sheet_surface_model(project_root, processed_dir, models_dir, route_xy, epsg) + models = build_sheet_surface_model( + project_root, processed_dir, models_dir, route_xy, f"EPSG:{epsg}" + ) if not models: raise ValueError("도엽등고선으로 지표면을 만들지 못했습니다 — 도엽 확보를 확인하세요.") diff --git a/B04_PreProcess/B04_PreProcess_Router_Inflow.py b/B04_PreProcess/B04_PreProcess_Router_Inflow.py index ab520978..9d72165a 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Inflow.py +++ b/B04_PreProcess/B04_PreProcess_Router_Inflow.py @@ -25,10 +25,7 @@ from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import STAGES, draina from B04_PreProcess.B04_PreProcess_Engine_Watershed_Flow import polygonize_labels from B04_PreProcess.B04_PreProcess_Engine_Watershed_Grid import GridSpec from B05_Profile.B05_Profile_Repository import get_surface_crs_epsg -from common_util.common_util_route_geometry import ( - find_planned_route_file, - read_planned_route, -) +from common_util.common_util_route_geometry import load_design_route from common_util.common_util_storage import resolve_stored_project_path from config.config_db import get_db_pool @@ -74,15 +71,25 @@ def _load_routing(stored_path: str) -> dict[str, Any] | None: async def _resolve_epsg(project_id: UUID, stored_path: str) -> str: - """분석에 쓰인 좌표계를 그대로 되찾는다(노선 파일이 명시하면 그 값 우선).""" + """분석에 쓰인 좌표계를 그대로 되찾는다. + + 격자 산출물은 `load_design_route()`가 맞춘 **사업지(.prj) 좌표계**에 있다. 노선 CSV의 + `crs_epsg` 열은 표시용 라벨이라 그 값을 쓰면 좌표가 딴 곳으로 간다 + (2026-09-01 실측: 라벨 5179, 실제 5176 — 유입 폴리곤이 1,500km 밖에 찍혔다). + """ + from B04_PreProcess.B04_PreProcess_Engine_Extent import project_epsg_from_prj + + project_root = Path(resolve_stored_project_path(stored_path)) + planned = load_design_route(project_root) + if planned is not None and planned.crs_input: + return planned.crs_input + prj_crs = project_epsg_from_prj(project_root) + if prj_crs: + return prj_crs pool = get_db_pool() async with pool.acquire() as connection: epsg = await get_surface_crs_epsg(connection, project_id, 0) - project_root = Path(resolve_stored_project_path(stored_path)) - route_file = find_planned_route_file(project_root / "B03_FileInput" / "input") - planned = read_planned_route(route_file) if route_file else None - source = (planned.epsg if planned else None) or epsg or 5186 - return f"EPSG:{source}" + return f"EPSG:{epsg or 5186}" def _collect_inflow( diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support.py b/B07_DesignDetail/B07_DesignDetail_Router_Support.py index da5d0526..6309b34b 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support.py @@ -206,7 +206,7 @@ def watershed_source(context: Any) -> dict[str, Any]: 저장본은 전부 WGS84라 여기서 미터 좌표로 되돌린다(B04가 저장할 때와 반대 방향). """ - to_metric = Transformer.from_crs("EPSG:4326", f"EPSG:{context.epsg}", always_xy=True) + to_metric = Transformer.from_crs("EPSG:4326", context.crs, always_xy=True) def metric(point: tuple[float, float]) -> tuple[float, float]: x, y = to_metric.transform(point[0], point[1]) diff --git a/common_util/common_util_drainage_context.py b/common_util/common_util_drainage_context.py index 364704b7..bb71200f 100644 --- a/common_util/common_util_drainage_context.py +++ b/common_util/common_util_drainage_context.py @@ -52,7 +52,9 @@ class DrainageContext: project_root: Path vertices: list[RouteVertex] = field(default_factory=list) z_source: str = Z_SOURCE_CSV - epsg: int = 5186 + # 사업지 좌표계 — pyproj 입력 문자열(`EPSG:n` 또는 .prj WKT). 노선 CSV의 `crs_epsg` + # 열은 표시용 라벨이라 실좌표계와 다를 수 있다(2026-09-01 실측: 라벨 5179, 실제 5176). + crs: str = "EPSG:5186" route_id: int | None = None to_lonlat: Callable[[float, float], tuple[float, float]] = lambda x, y: (x, y) @@ -96,15 +98,17 @@ async def load_drainage_context(project_id: UUID) -> tuple[DrainageContext | Non sampler=sampler, ) - epsg = int(planned.epsg or db_epsg or 5186) - transformer = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True) + # 노선을 실제로 담고 있는 좌표계를 쓴다 — `load_design_route()`가 .prj 좌표계로 + # 재투영하며 `crs_input`만 갱신하고 `epsg` 라벨은 CSV 값 그대로 남긴다. + crs = planned.crs_input or f"EPSG:{planned.epsg or db_epsg or 5186}" + transformer = Transformer.from_crs(crs, "EPSG:4326", always_xy=True) return ( DrainageContext( stored_path=stored_path, project_root=project_root, vertices=vertices, z_source=z_source, - epsg=epsg, + crs=crs, route_id=int(route["id"]) if route else None, to_lonlat=lambda x, y: transformer.transform(x, y), ),