fix(B05): 6/7/8 단계 산출물도 영구저장소에 남긴다

확인해 보니 7(2차 유역 외곽선)과 8(기본 관)은 저장되고 있었으나
6(흐름 강도 곡선)이 빠져 있었고, 관에 누가거리가 안 붙었다.

- write_stage 의 레이어 항목이 (기하, 속성dict) 짝도 받도록 확장.
  관 피처에 chainage_m / reason / stream_name 이 실린다.
- 흐름 강도 곡선은 기하가 아니라 수치 곡선이라 GeoJSON 대신
  02_flow_direction.npz 에 strength_chainage_m / strength_area_m2 로 담는다.
- manifest 에 strength_points / strength_total_m2 요약 추가.

저장 확인 (실데이터)
  01_primary_region.geojson  688KB  kind: primary_region 1 / upstream 16 /
     downstream 860 / route 1 / grid_bbox 1 / basin_boundary 1 / pipe 1
     pipe 속성 = chainage_m 149.73, reason stream
  01_primary_region.npz       11KB  셀 마스크 + 확장 회차
  02_flow_direction.npz     2353KB  direction/reaches_road/analyzed/receiver/
     burned/band_elevation + 강도 곡선 71점(합계 457,404m2, 최대 150m)
  manifest.json                     3단계 요약 전부

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 20:04:26 +09:00
co-authored by Claude Fable 5
parent baef88dd50
commit b0747e74b6
2 changed files with 34 additions and 10 deletions
@@ -44,13 +44,14 @@ def drainage_dir(stored_path: str) -> Path:
def write_stage(
stored_path: str,
stage: str,
layers: dict[str, Sequence[BaseGeometry]],
layers: dict[str, Sequence[BaseGeometry | tuple[BaseGeometry, dict[str, Any]]]],
properties: dict[str, Any],
to_lonlat: LonLat,
) -> str | None:
"""한 단계의 기하 산출물을 GeoJSON으로 저장하고 매니페스트를 갱신한다.
`layers`는 {레이어이름: 사업지 CRS(m) 기하 목록}이며 피처 `kind` 속성이 된다.
`layers`는 {레이어이름: 사업지 CRS(m) 기하 목록}이며 레이어 이름이 피처 `kind` 속성이
된다. 기하 대신 `(기하, 속성dict)` 짝을 넣으면 그 속성이 피처에 함께 실린다.
좌표는 여기서 WGS84로 바꾼다 — 저장 파일은 어떤 도구로 열어도 바로 보여야 한다.
"""
prefix = STAGES.get(stage)
@@ -60,12 +61,14 @@ def write_stage(
features: list[dict[str, Any]] = []
counts: dict[str, int] = {}
for kind, geometries in layers.items():
for index, geometry in enumerate(geometries):
feature = _to_feature(kind, index, geometry, to_lonlat)
for kind, entries in layers.items():
for index, entry in enumerate(entries):
# 항목은 기하 하나이거나 (기하, 속성) 짝이다 — 관 누가거리처럼 붙일 값이 있을 때 쓴다.
geometry, extra = entry if isinstance(entry, tuple) else (entry, None)
feature = _to_feature(kind, index, geometry, to_lonlat, extra)
if feature is not None:
features.append(feature)
counts[kind] = len(geometries)
counts[kind] = len(entries)
filename = f"{prefix}_{stage}.geojson"
directory = drainage_dir(stored_path)
@@ -90,14 +93,18 @@ def write_stage(
def _to_feature(
kind: str, index: int, geometry: BaseGeometry, to_lonlat: LonLat
kind: str,
index: int,
geometry: BaseGeometry,
to_lonlat: LonLat,
extra: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
coordinates = _to_lonlat_coords(geometry, to_lonlat)
if coordinates is None:
return None
return {
"type": "Feature",
"properties": {"kind": kind, "index": index},
"properties": {"kind": kind, "index": index, **(extra or {})},
"geometry": {"type": geometry.geom_type, "coordinates": coordinates},
}
+19 -2
View File
@@ -279,9 +279,19 @@ async def get_primary_region(project_id: UUID) -> dict[str, Any] | JSONResponse:
"downstream": region.split.downstream,
"route": [prepared["route_line"]],
"grid_bbox": [_grid_bbox_polygon(spec)],
# ⑦ 2차 전체 배수유역 외곽선, ⑧ 기본 관 위치.
# ⑦ 2차 전체 배수유역 외곽선, ⑧ 기본 관 위치(누가거리·근거 포함).
"basin_boundary": _boundary_geometry(preview.basin_boundary_xy),
"pipe": [Point(pipe.x, pipe.y) for pipe in preview.pipes],
"pipe": [
(
Point(pipe.x, pipe.y),
{
"chainage_m": round(pipe.chainage_m, 2),
"reason": pipe.reason,
"stream_name": pipe.stream_name,
},
)
for pipe in preview.pipes
],
},
{
"radius_m": region.radius_m,
@@ -331,6 +341,11 @@ def _write_stage_arrays(stored_path: str, preview: Any, domain: Any, spec: Any)
arrays["burned"] = flow.burned.reshape(spec.n_rows, spec.n_cols)
if preview.descent is not None:
arrays["band_elevation"] = preview.descent.band_elevation
# ⑥ 흐름 강도 곡선 — 기하가 아니라 수치 곡선이라 GeoJSON이 아닌 여기에 함께 담는다.
if preview.strength_profile:
curve = np.asarray(preview.strength_profile, dtype=np.float64)
arrays["strength_chainage_m"] = curve[:, 0]
arrays["strength_area_m2"] = curve[:, 1]
write_grid_arrays(
stored_path,
"flow_direction",
@@ -346,6 +361,8 @@ def _write_stage_arrays(stored_path: str, preview: Any, domain: Any, spec: Any)
"burned": 0 if flow.burned is None else int(flow.burned.sum()),
"outer_seeds": flow.outer_seeds,
"interior_seeds": flow.interior_seeds,
"strength_points": len(preview.strength_profile),
"strength_total_m2": round(sum(area for _, area in preview.strength_profile), 1),
},
)