- 판정 = 저장 · 새 계획 폴리라인 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>
169 lines
7.5 KiB
Python
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}"}
|