fix(B04): 노선 CSV의 EPSG 라벨 대신 사업지 실좌표계로 화면 좌표를 만든다
상세 배수유역 폴리곤이 지도에 안 그려졌다. 노선 CSV `crs_epsg` 열(라벨 5179)을 좌표계로 써서, 사업지 좌표계(.prj EPSG:5176)로 계산된 격자 산출물을 잘못 역투영한 탓이다 — 유역이 lon 119.79 / lat 23.08(대만 남쪽 바다)로 나가 화면 밖이었다. `load_design_route()`는 노선을 .prj 좌표계로 재투영하며 `crs_input`만 갱신하고 `epsg` 라벨은 CSV 값 그대로 둔다(2026-08-31 확정). 그 라벨을 좌표계로 쓰던 자리를 모두 `crs_input`(= .prj 좌표계)으로 바꿨다. - common_util_drainage_context: DrainageContext.epsg(int) → crs(str, pyproj 입력). 상세 배수유역·관 지점 응답 좌표가 노선 위로 돌아온다. - B07_DesignDetail_Router_Support: context.crs 그대로 사용. - SheetSurface.build_sheet_surface_from_route: load_design_route로 노선을 읽어 도엽 서피스를 라이다 지표면과 같은 좌표계에 만든다(기존엔 5179 격자로 만들어져 라이다 DTM과 다른 프레임에 놓였다). LAS 없는 WF1 경로는 라벨·좌표가 한 벌이라 그대로. - Router_Inflow._resolve_epsg: 격자 산출물과 같은 .prj 좌표계로 역투영. 검증: tmp/tests/test_drainage_crs.py 신규(용화 샘플 회귀) + tmp/tests 전체 37 passed. build_detail 실행 결과 유역 11개 폴리곤이 lon 129.0947~129.0988 / lat 36.8107~36.8127 (노선 위)로 나온다 — 수정 전 같은 좌표는 119.7872 / 23.0817이었다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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("도엽등고선으로 지표면을 만들지 못했습니다 — 도엽 확보를 확인하세요.")
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user