"""추천 후보 초기 종단 모으기(476) — `POST /route/initial` 결과에 `initial_profile` 을 붙임. 입력 모으기(DB · 파일) · 지문 · 저장본 = 여기 · 셈 조각 = `B05_Profile_Engine_InitialProfile`(머리에 흐름 · 응답 칸). `B05_Profile_Router_RouteInitial`(700줄)에서 나눔 — `compute_initial` 이 부름. """ from __future__ import annotations import asyncio import functools import logging import time from pathlib import Path from typing import Any from uuid import UUID from fastapi.responses import JSONResponse from config.config_db import get_db_pool logger = logging.getLogger(__name__) async def initial_profile_of( project_id: UUID, project_root: Path, stored_path: str, result: dict[str, Any], request: Any, selection: dict[str, Any], key: str, ) -> dict[str, Any]: """추천 후보 초기 종단(476 · `B05_Profile_Engine_InitialProfile` 머리) — [적용] 체인과 같은 입력 · 같은 조각 · 지문 같으면 저장본 꺼냄. 실패 = `{error}`.""" from dataclasses import asdict import aiomysql from B03_FileInput.B03_FileInput_Service_Chain import redesign_solve_request from B04_PreProcess.B04_PreProcess_Router_Watershed import STREAM_FILE, _prepare, _sheet_dir from B05_Profile import B05_Profile_Engine_InitialProfile as ip from B05_Profile import B05_Profile_Router_Replan as replan from B05_Profile.B05_Profile_Engine_Sections import _MODELS_SUBDIR from B05_Profile.B05_Profile_Repository import get_latest_route from B05_Profile.B05_Profile_Route_Save import section_seed from B05_Profile.B05_Profile_Router_RouteInitial import balanced_profile_of, project_standard from B06_Section.B06_Section_Repository import ( get_latest_section_options, get_route_generation_source, ) from B06_Section.B06_Section_Router_Regenerate_Options import ( _regeneration_grade_options, _regeneration_options, ) from common_util.common_util_surface_sampler import build_surface_sampler from common_util.common_util_workflow_state import get_workflow_state started = time.perf_counter() try: radius_m, _, _ = await replan._plan_criteria(project_id) # [적용] 노선 짜기와 같은 R line, curves = ip.candidate_line(result["nodes"], result.get("curves") or [], radius_m) pool = get_db_pool() async with pool.acquire() as connection: latest = await get_latest_route(connection, project_id) source = ( await get_route_generation_source(connection, project_id, int(latest["id"])) if latest else None ) old_options = await get_latest_section_options(connection, project_id) async with connection.cursor(aiomysql.DictCursor) as cursor: state = await get_workflow_state(cursor, str(project_id)) if not latest: return {"error": "저장된 노선이 없어 초기 종단을 셀 수 없습니다(자동설계 먼저)."} stage2 = next((s for s in state.get("stages", []) if int(s.get("stage_no", -1)) == 2), None) params = dict((stage2 or {}).get("params") or {}) first, last = line[0], line[-1] # 체인 = 새 노선 점으로 stage 2 를 세움 — 측점 간격 · 계획선 기준엔 무관 params["points"] = { "bp": {"x": first[0], "y": first[1]}, "ep": {"x": last[0], "y": last[1]}, } model_id = int(latest.get("surface_model_id") or 0) seed = section_seed(redesign_solve_request(params, selection, model_id), old_options) options = _regeneration_options(seed, params, float(seed["cross_half_width_m"])) grade_options = _regeneration_grade_options(params) epsg = (source or {}).get("crs_epsg") crs = f"EPSG:{epsg}" if epsg is not None else None surface = {k: selection.get(k) for k in ("source_filter", "method", "smooth")} models = project_root / _MODELS_SUBDIR route_path = str(latest.get("route_data_path") or "") mark = ip.fingerprint( { "key": key, "nodes": result["nodes"], "curves": result.get("curves"), "radius": radius_m, "surface": surface, "models": ip.stat_marks(list(models.glob(f"*_{surface['source_filter']}*.npz"))), "streams": ip.stat_marks([_sheet_dir(stored_path) / STREAM_FILE]), # 초기화(492) = 저장 관 · 구조물 · 추가 측점을 안 씀 — 노선 파일만 "files": ip.file_marks([project_root / route_path]), "options": asdict(options), "grade": grade_options.as_dict() if grade_options else None, "crs": crs, "with_profile": request.with_profile, "criteria": request.criteria.model_dump() if request.criteria else None, "standard": await project_standard(project_id), "road": await replan._road_settings(project_id), } ) stored = await asyncio.to_thread(ip.read_initial, project_root, key, mark) if stored is not None: return stored prepared = await _prepare(project_id) streams = [] if isinstance(prepared, JSONResponse) else prepared["streams"] facilities = await asyncio.to_thread(ip.reset_facilities, line, streams) target = None if request.with_profile: # [적용] `balanced_target` 과 같은 셈(노선 정점 · 새 곡선표) points = [{"x": x, "y": y} for x, y in line] balanced = await balanced_profile_of( project_id, project_root, points, request.criteria, curves ) target = None if balanced is None else (balanced["stations_m"], balanced["plan_m"]) sampler = await asyncio.to_thread( build_surface_sampler, models, str(surface["source_filter"]), str(surface["method"] or "dtm"), bool(surface["smooth"]), ) longitudinal = await asyncio.to_thread( functools.partial( ip.initial_longitudinal, project_root, line, curves, sampler, facilities, options=options, grade_options=grade_options, source_snapshot={ "filter": surface["source_filter"], "method": surface["method"], "smooth": bool(surface["smooth"]), }, crs=crs, target=target, ) ) summary = { **ip.profile_summary(longitudinal), "with_profile": bool(request.with_profile), "seconds": round(time.perf_counter() - started, 2), } await asyncio.to_thread(ip.write_initial, project_root, key, mark, summary) logger.info( "추천 초기 종단 셈: project_id=%s %s 꼭짓점 %s 직선화 %s %.2fs", project_id, request.mode, summary.get("pvi_count"), summary.get("straightened"), summary["seconds"], ) return summary except Exception as error: # noqa: BLE001 — 추천은 그대로 · 화면이 이유 한 줄 logger.exception("추천 초기 종단 셈 실패: project_id=%s", project_id) return {"error": f"초기 종단을 셈하지 못했습니다: {error}"}