diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support.py b/B07_DesignDetail/B07_DesignDetail_Router_Support.py index f4e399d0..ac689b28 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support.py @@ -41,7 +41,6 @@ from B07_DesignDetail.B07_DesignDetail_Schema import ( DesignDrawingItem, ) from common_util.common_util_drainage_pipes import detail_basins_path -from common_util.common_util_route_geometry import find_planned_route_file, read_planned_route from common_util.common_util_route_profile import design_elevation_from_longitudinal from config.config_system import DRAWING_SCALE_BASIN @@ -133,6 +132,11 @@ def _basins_crs(context: Any, payload: dict[str, Any]) -> str: 파일이라 노선 CSV의 EPSG 라벨로 쓰였다 — 그 라벨로 되돌려야 왕복이 맞는다 (2026-09-01). 라벨을 못 읽으면 현재 사업지 좌표계로 둔다. """ + from common_util.common_util_route_geometry import ( + find_planned_route_file, + read_planned_route, + ) + stored = payload.get("crs_input") if isinstance(stored, str) and stored: return stored @@ -229,13 +233,35 @@ def clip_line_to_box( return [run for run in runs if len(run) >= 2] +# 세부유역이 노선에서 이만큼 넘게 떨어져 있으면 좌표계를 잘못 되돌린 것으로 본다. +# 임도 한 노선이 담는 유역은 길어야 수 km라 오검출 여지가 없다. +_BASIN_MAX_DISTANCE_M = 50_000.0 + + +def _too_far_from_route( + ring: list[tuple[float, float]], route_xy: list[tuple[float, float]] +) -> bool: + """되돌린 유역이 노선 근처에 없으면 True — 좌표계를 되찾지 못한 저장본이다.""" + if not ring or not route_xy: + return False + cx = sum(x for x, _ in ring) / len(ring) + cy = sum(y for _, y in ring) / len(ring) + return min(math.hypot(cx - x, cy - y) for x, y in route_xy) > _BASIN_MAX_DISTANCE_M + + def watershed_source(context: Any) -> dict[str, Any]: """유역도 입력(노선·세부유역·등고선·세류선)을 사업지 CRS(m)로 모은다. 저장본은 전부 WGS84라 여기서 미터 좌표로 되돌린다(B04가 저장할 때와 반대 방향). + + 되돌리는 좌표계가 둘이다. **배경(도엽 등고선·세류선)은 사업지 좌표계**로 돌린다 — + 노선(`context.vertices`)이 그 좌표계에 있으므로 같은 자리에 겹쳐야 한다. **세부유역은 + 그 파일을 쓸 때 쓴 좌표계**로 돌린다 — 좌표계 기록 이전 저장본은 노선 CSV의 EPSG + 라벨로 쓰였고, 그 라벨로 되돌려야 원래 미터 좌표가 나온다(2026-09-01). """ basins_payload = _geojson_payload(detail_basins_path(context.stored_path)) - to_metric = Transformer.from_crs( + to_metric = Transformer.from_crs("EPSG:4326", context.crs, always_xy=True) + to_basin_metric = Transformer.from_crs( "EPSG:4326", _basins_crs(context, basins_payload), always_xy=True ) @@ -243,8 +269,13 @@ def watershed_source(context: Any) -> dict[str, Any]: x, y = to_metric.transform(point[0], point[1]) return (float(x), float(y)) + def basin_metric(point: tuple[float, float]) -> tuple[float, float]: + x, y = to_basin_metric.transform(point[0], point[1]) + return (float(x), float(y)) + route_xy = [(vertex.x, vertex.y) for vertex in context.vertices] basins: list[dict[str, Any]] = [] + dropped = 0 for feature in basins_payload.get("features") or []: properties = feature.get("properties") or {} if properties.get("kind") != "detail_basin": @@ -252,7 +283,21 @@ def watershed_source(context: Any) -> dict[str, Any]: rings = _geometry_lines(feature.get("geometry")) if not rings: continue - basins.append({"ring": [metric(point) for point in rings[0]], "props": properties}) + ring = [basin_metric(point) for point in rings[0]] + # 저장본 좌표계를 못 되찾으면 유역이 노선에서 수백 km 밖으로 떨어진다. 그대로 두면 + # 도곽이 그 거리까지 벌어져 도면이 통째로 빈 화면이 된다 — 유역만 버리고 배경·노선은 + # 그린다(2026-09-01 다른 PC 보고: 유역도 그림 자체가 없음). + if _too_far_from_route(ring, route_xy): + dropped += 1 + continue + basins.append({"ring": ring, "props": properties}) + if dropped: + logger.warning( + "B07 유역도: 노선에서 %.0fkm 넘게 떨어진 세부유역 %d개를 뺐습니다 — " + "저장본 좌표계를 되찾지 못했습니다. B04에서 유역을 다시 확정하세요.", + _BASIN_MAX_DISTANCE_M / 1000.0, + dropped, + ) # 배경(등고선·세류선)은 **여러 도엽을 합쳐 받은 뒤 도곽 크기로 절취**한다 # (2026-08-30 사용자 지시 — 노선이 도엽 경계에 걸릴 수 있어 주변 도엽까지 받아 둔다).