Files
Aislo/B05_Profile/B05_Profile_Router_RouteInitial_Initial.py
T
eomsangdonandClaude Opus 5.5 7a857d626a fix(492-1 노선 바뀜 초기화): 노선 모양이 바뀐 [저장] · 추천 [적용] = 서버 초기화 + 초기값 다시 뜸
- 판정 = 저장 · 새 계획 폴리라인 1 mm 견줌(`route_shape_changed` · TS 짝 `routeShapeChanged` = sub1) · 스냅 · 간격만 = 따라감 그대로
- 체인 초기화 = 사용자 구조물 · 추가 측점 · 관(옵션 포함) 버림 · 관 = 새 노선 세류선 교차만 · 1차 계획선 · 횡단 기본 설계 · 응답 route_reset {reset, dropped}
- 한 본문 초기화 = 보낸 관 · 구조물 · 추가 측점 · 계획고 · 관 옵션 · 넘김 칸 안 씀(페이지 설정만) · 되돌림 범위에 initial_snapshot
- 추천 초기 종단 미리 셈 = 같은 초기화 시설 · 지문에서 시설 파일 뺌
- 시험: 새 test_283_route_change_reset(옛 코드 8 실패) · 검증_sub8 자료 고정 · 실DB 시험 둘 = storage 임시 사본(helper_temp_storage) · 따라감 기대 시험 문구 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:12:35 +09:00

169 lines
7.5 KiB
Python

"""추천 후보 초기 종단 모으기(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}"}