Files
Aislo/B04_PreProcess/B04_PreProcess_Router_Watershed_Live.py
T
eomsangdonandClaude Opus 5.5 d5d6415966 feat(배수유역): 347-2 「지금 노선으로 셈」 서버 길 · 347-3 관 자리 후보 · [확정] 다시 셈
- POST drainage/live — 몸통 = 차례 번호 + 노선 초안(RouteDraft · 노선 [확인]과 같은 폴리라인 규칙) + 관 목록 · 서버 단독 · 작업본(00_watershed_response · 격자 배열 · 관 지점)에 안 씀 · 세부 유역까지 메모리 격자로 한 요청
- 프로젝트별 마지막 차례 · 1차 영역 뒤 · 확장 회차마다 · 세부 유역 앞에서 바뀌었으면 그만둠(superseded · AnalysisStopped)
- build_detail 에 메모리 routing · 상류선 · 관 자리 후보 suggest_pipe_positions(간격 보충 규칙 · 흐름 세기 + 오목 · 기존 관에서 먼 곳 · 정수 m) · 응답 pipe_candidates
- PUT pipe-points ?recompute=true([확정]) = 낡았으면 배수유역부터 다시 셈 · [저장]은 낡음 달고 관만
- 시험 test_347_2_drainage_live(늦은 차례 그만둠 둘 · 엔진 회차 그만둠 · 메모리 격자 · 후보 · 확정 다시 셈) · 검증_sub3 실측 저장 노선 16.9 s · 초안 노선 11.5 s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
2026-10-16 11:38:17 +09:00

153 lines
6.9 KiB
Python

"""「지금 노선으로 셈」(347-2) — 노선 초안 + 관 자리로 배수유역 · 세부 유역을 한 요청에 셈.
서버 단독(③) · **작업본에 안 씀** — `00_watershed_response.json` · 격자 배열 · 관 지점은 그대로이고
답으로만 돌려준다(파생 저장본 = 흐름 방향 · 도엽 좌표만 · 347-1). 세부 유역 나눔까지 같은 요청에서
메모리 격자로 해 격자를 화면에 넘기지 않는다.
프로젝트마다 「마지막 차례」를 적어 두고 단계 사이(1차 영역 뒤 · 확장 회차마다 · 세부 유역 앞)에서
차례가 바뀌었으면 그만둔다(`superseded`). 화면은 늦은 답을 차례 · 노선 지문으로 한 번 더 거른다.
라우터는 `B04_PreProcess_Router_Watershed.router` 에 붙는다(그 파일 끝에서 읽음).
"""
from __future__ import annotations
import asyncio
import math
from dataclasses import replace
from typing import Any
from uuid import UUID
from fastapi.responses import JSONResponse
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import preview_stages
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Expand import AnalysisStopped
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import drainage_dir
from B04_PreProcess.B04_PreProcess_Router_Basins import _build, _payload
from B04_PreProcess.B04_PreProcess_Router_Watershed import _prepare, router
from common_util.common_util_drainage_context import _open_sampler, load_drainage_context
from common_util.common_util_drainage_detail import RoadRouting
from common_util.common_util_drainage_pipes import parse_pipe_points
from common_util.common_util_route_geometry import RouteVertex, build_route_vertices
from common_util.common_util_route_polyline import build_planned_polyline
from common_util.common_util_route_profile import resolve_route_profile
from config.config_system import DRAINAGE_ARROW_SPACING_M
# 프로젝트 → 마지막으로 받은 차례 번호. ponytail: 서버 프로세스 하나 전제(여럿이면 DB · 파일로).
_LATEST_GENERATION: dict[str, int] = {}
SUPERSEDED = {"status": "superseded"}
def draft_vertices(draft: dict[str, Any], min_radius_m: float) -> list[RouteVertex]:
"""노선 초안(RouteDraft) → 그린 노선 정점(z 없음) — 노선 [확인]과 같은 폴리라인 규칙.
ponytail: 붙든 곡선 길이(curveLock · curveArc) · 측점 스냅은 안 봄 — 곡선 중점이 조금 다를 뿐
배수유역 결과엔 셀 몇 개 차이. 필요하면 노선 [확인] 길과 같이 넘김.
"""
nodes = [(float(x), float(y)) for x, y in draft["nodes"]]
flags = [bool(flag) for flag in draft.get("curveOn") or [True] * len(nodes)]
radii = [None if r is None else float(r) for r in draft.get("curveRadius") or []]
polyline = build_planned_polyline(
nodes, min_radius_m=min_radius_m, simplify=False, curve_flags=flags, radii=radii or None
)
return build_route_vertices([{"x": x, "y": y} for x, y in polyline.vertices])
def live_routing(preview: Any, route_xy: list[tuple[float, float]], to_lonlat: Any) -> RoadRouting:
"""분석 결과(메모리) → 세부 유역 나눔 입력 — 파일 길(`_write_road_routing` → 읽기)과 같은 값."""
routing, road = preview.routing, preview.road
return RoadRouting(
spec=preview.spec,
road_slot=routing.road_slot,
path_length=routing.path_length,
elevation=preview.terrain.elevation.reshape(-1),
road_cell_index=road.cell_index,
road_chainage=road.chainage,
strength=routing.strength,
route_lonlat=[list(to_lonlat(x, y)) for x, y in route_xy],
basin_lonlat=[list(to_lonlat(x, y)) for x, y in preview.basin_boundary_xy],
base_pipes=list(preview.pipes),
flow_arrows=[
[round(x, 2), round(y, 2), round(math.degrees(angle), 1), reaches, cells]
for x, y, angle, reaches, cells in preview.flow_arrows
],
arrow_spacing_m=DRAINAGE_ARROW_SPACING_M,
)
@router.post("/{project_id}/drainage/live", response_model=None)
async def post_drainage_live(
project_id: UUID, payload: dict[str, Any] | None = None
) -> dict[str, Any] | JSONResponse:
"""몸통 = {generation, route: RouteDraft | null(저장 노선), points: 관 목록 | null(자동)}."""
body = payload or {}
generation = int(body.get("generation") or 0)
key = str(project_id)
_LATEST_GENERATION[key] = generation
def superseded() -> bool:
return _LATEST_GENERATION.get(key) != generation
try:
raw = body.get("points")
points = None if raw is None else parse_pipe_points(raw)
except ValueError as error:
return JSONResponse(status_code=400, content={"status": "error", "message": str(error)})
prepared = await _prepare(project_id)
if isinstance(prepared, JSONResponse):
return prepared
context, reason = await load_drainage_context(project_id)
if context is None:
return JSONResponse(status_code=404, content={"status": "error", "message": reason})
vertices = context.vertices
draft = body.get("route")
if draft and len(draft.get("nodes") or []) >= 2:
from B05_Profile.B05_Profile_Router_Replan import _plan_criteria
radius_m, *_ = await _plan_criteria(project_id)
drawn = await asyncio.to_thread(draft_vertices, draft, radius_m)
sampler = await asyncio.to_thread(
_open_sampler, context.project_root, context.surface_params
)
vertices, _ = await asyncio.to_thread(resolve_route_profile, drawn, sampler=sampler)
if superseded():
return SUPERSEDED
try:
preview = await asyncio.to_thread(
preview_stages,
vertices,
prepared["contours"],
prepared["streams"],
(drainage_dir(context.stored_path), prepared["contour_key"]),
superseded,
)
except AnalysisStopped:
return SUPERSEDED
if preview is None or preview.routing is None:
return JSONResponse(
status_code=400,
content={"status": "error", "message": "지금 노선으로 배수유역을 정하지 못했습니다."},
)
if superseded():
return SUPERSEDED
live = replace(context, vertices=vertices)
to_lonlat = context.to_lonlat
routing = live_routing(preview, [(v.x, v.y) for v in vertices], to_lonlat)
upstream = [
[list(to_lonlat(x, y)) for x, y in line.coords] for line in preview.region.split.upstream
]
detail, carried = await asyncio.to_thread(
_build, context.stored_path, live, points, routing, upstream
)
if detail is None:
return JSONResponse(
status_code=400,
content={"status": "error", "message": "세부 유역을 나누지 못했습니다."},
)
result = _payload(project_id, live, detail, carried, False)
result["generation"] = generation
result["live"] = True
return result