fix(B07): 유역도 배경은 사업지 좌표계로, 세부유역은 저장 당시 좌표계로 되돌린다
좌표계 기록 이전 저장본을 되돌리는 폴백을 넣으면서 변환을 하나로 묶었다. 그래서 도엽 배경(등고선·세류선)까지 저장본 좌표계로 되돌아가, 라벨과 실좌표계가 갈린 사업지에서 배경이 1,000km 밖에 놓였다. 도곽이 그 거리까지 벌어지면 유역도는 사실상 빈 화면이 된다 (다른 PC 보고: 배수유역도 그림 자체가 없음). 변환을 둘로 나눈다. - 배경·노선: `context.crs`(사업지 .prj 좌표계) — 노선이 그 좌표계에 있다. - 세부유역: 그 파일을 쓸 때 쓴 좌표계 — 옛 저장본은 노선 CSV의 EPSG 라벨로 쓰였고, 같은 라벨로 되돌려야 원래 미터 좌표가 나온다(왕복이 상쇄된다). 되찾을 근거조차 없을 때(노선 파일 없음 등)를 위해 안전장치를 뒀다. 노선에서 50km 넘게 떨어진 유역은 빼고 경고를 남긴다 — 도면 전체를 잃는 대신 배경·노선은 그린다. 폴백에 쓰는 route_geometry import 는 함수 안으로 내렸다(순환 import 여지 제거). 검증: tmp/tests 44 passed(신규 test_watershed_source_crs.py 2건). c1bb453f 유역도 API 재확인 — 엔티티 1,597개(등고선 1028·계류 460·배수유역 20·도각 82· 유역 제원 4·노선 1·표제 2)로 수정 전과 같다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 사용자 지시 — 노선이 도엽 경계에 걸릴 수 있어 주변 도엽까지 받아 둔다).
|
||||
|
||||
Reference in New Issue
Block a user