feat(B05): 1차 배수유역을 상류 세류망 기준으로 재정의 + 단계 검증 화면

1차 영역이 도로 전체 버퍼까지 포함해 도로 아래(하류)로 퍼지고, 세류선을 피처
단위로만 갈라 상류망이 통째로 누락되던 문제를 고친다.

- 1차 영역 = 도로 교차점 상류로 이어진 세류망만 반경 버퍼. 노선은 버퍼하지 않음.
  노선이 영역 밖으로 나간 길이(road_outside_m)를 재서 반경 판단 근거로 노출.
- 상류 판정을 연결망 기준으로 교체: unary_union 노딩 -> 도로에서 절단 ->
  끝점 그래프 -> 도로 교차 노드를 통과하지 않는 확산. T자로 붙은 지류와
  2단계 이상 이어진 지류까지 상류망으로 따라간다.
- 상하류 판정 표고를 최근접 등고선 정점에서 TIN 선형보간으로 교체
  (ElevationSampler). 최근접 정점은 오차가 등고선 간격만큼 나서 계곡 교차점이
  한 등고선 위로 잡히고 상류 조각이 전부 하류로 오판됐다.
- 격자 크기 자동 강등 삭제 - config 값을 그대로 쓴다. 셀 수가 많으면 경고만.
  자동 강등이 도로 굽기 두께 전제를 조용히 깨뜨렸다.
- TIN 삼각망을 격자 범위 + 여유로 클리핑. 결과 동일, 속도만 개선.
- 기본 반경 300m -> 50m (단계 검증용).

단계 검증 수단
- GET /drainage/primary-region : TIN/흐름 계산 없이 상류망/하류망/1차영역/격자만 반환
- 프론트 1차영역 버튼 : 상류망(굵은 파랑), 하류망(회색 파선), 1차 영역(초록 채움),
  해석 격자를 실제 셀 눈금으로 렌더. 켤 때마다 재요청한다.
- 클릭 시 storage/{project}/B05_wf2_Route/drainage/primary_region.geojson 저장

합성 검증(T자 지류 + 2단계 지류 + 하류 지류 + 고아 세류):
상류망 4조각 채택 / 하류망 3조각 / 미연결 1개 제외 - 전부 기대대로.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 17:44:53 +09:00
co-authored by Claude Fable 5
parent c1f139527d
commit e1b410c391
6 changed files with 685 additions and 79 deletions
+10 -3
View File
@@ -242,13 +242,15 @@ SKELETON_NODE_SPACING_M = float(os.getenv("SKELETON_NODE_SPACING_M", "10.0"))
# ─────────────────────────────────────────────────────────────────────────
# 해석 격자 한 변(m). 작을수록 정밀하나 셀 수가 제곱으로 늘어난다.
DRAINAGE_GRID_SIZE_M = float(os.getenv("DRAINAGE_GRID_SIZE_M", "1.0"))
# 1차 배수유역 반경(m). 정리된 세류선과 노선을 이 반경으로 버퍼해 초기 해석 범위를 잡는다.
DRAINAGE_INITIAL_RADIUS_M = float(os.getenv("DRAINAGE_INITIAL_RADIUS_M", "300.0"))
# 1차 배수유역 반경(m). 도로 교차점 상류로 이어진 세류망을 이 반경으로 버퍼한 범위가
# 1차 영역이며 그 bbox가 해석 격자다. 노선은 버퍼하지 않는다(2026-07-31 사용자 지시).
DRAINAGE_INITIAL_RADIUS_M = float(os.getenv("DRAINAGE_INITIAL_RADIUS_M", "50.0"))
# 활성 셀이 격자 최외곽에 닿았을 때 한 번에 넓히는 폭(m).
DRAINAGE_EXPAND_STEP_M = float(os.getenv("DRAINAGE_EXPAND_STEP_M", "200.0"))
# 확장 반복 상한. 경계 링이 전부 비활성이 되면 그 전에 스스로 멈춘다(안전핀).
DRAINAGE_MAX_EXPAND_ROUNDS = int(os.getenv("DRAINAGE_MAX_EXPAND_ROUNDS", "6"))
# 격자 셀 수 상한. 초과하면 셀 크기 자동으로 키워 맞춘다(메모리 보호).
# 격자 셀 수 권장 상한. 넘으면 **경고만** 남기고 그대로 계산한다 — 격자 크기 자동 조절은
# 하지 않는다(2026-07-31 사용자 지시). 느리면 위 DRAINAGE_GRID_SIZE_M을 직접 올린다.
DRAINAGE_MAX_GRID_CELLS = int(os.getenv("DRAINAGE_MAX_GRID_CELLS", "16000000"))
# 도로 폭(m). 이 폭으로 노선을 격자에 구워 D8 흐름이 도로를 대각선으로 건너뛰지 못하게 한다.
DRAINAGE_ROAD_WIDTH_M = float(os.getenv("DRAINAGE_ROAD_WIDTH_M", "4.0"))
@@ -258,6 +260,9 @@ DRAINAGE_CONTOUR_MARGIN_M = float(os.getenv("DRAINAGE_CONTOUR_MARGIN_M", "10.0")
DRAINAGE_CONTOUR_MIN_LENGTH_M = float(os.getenv("DRAINAGE_CONTOUR_MIN_LENGTH_M", "20.0"))
# 등고선 정점 재샘플 간격(m). 조밀할수록 TIN이 정확하나 Delaunay 비용이 커진다.
DRAINAGE_CONTOUR_RESAMPLE_M = float(os.getenv("DRAINAGE_CONTOUR_RESAMPLE_M", "5.0"))
# TIN 삼각망을 만들 때 격자 범위 밖으로 남길 여유(m). 도엽 전체 등고선을 다 물면 삼각망
# 비용만 커지고 결과는 같다. 여유가 0이면 격자 가장자리가 TIN 밖으로 나가 NaN이 된다.
DRAINAGE_CONTOUR_CLIP_MARGIN_M = float(os.getenv("DRAINAGE_CONTOUR_CLIP_MARGIN_M", "100.0"))
# 평탄면 해소용 미세 경사(m/셀). 채움 후 흐름 방향이 없는 셀에 출구 쪽 경사를 만들어 준다.
DRAINAGE_FLAT_EPSILON_M = float(os.getenv("DRAINAGE_FLAT_EPSILON_M", "0.001"))
# 관 매설 최대 간격(m). 이 간격을 넘으면 흐름 강도가 가장 큰 지점에 관을 보충한다.
@@ -273,6 +278,8 @@ DRAINAGE_MIN_BASIN_AREA_M2 = float(os.getenv("DRAINAGE_MIN_BASIN_AREA_M2", "100.
# 격자 해석 결과 캐시 파일명. 프로젝트 저장소의 B05_wf2_Route/drainage/ 아래에 놓인다.
DRAINAGE_CACHE_DIRNAME = "drainage"
DRAINAGE_CACHE_FILENAME = "watershed_grid.npz"
# 1차 영역 검증 산출물. 버튼을 누를 때마다 덮어써서 사람이 QGIS 등으로 직접 열어볼 수 있게 한다.
DRAINAGE_REGION_FILENAME = "primary_region.geojson"
# ─────────────────────────────────────────────────────────────────────────