150 lines
6.4 KiB
Python
150 lines
6.4 KiB
Python
"""B04 지표면 분석 FastAPI 라우터."""
|
|
|
|
import asyncio
|
|
import logging
|
|
from pathlib import Path
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
|
from B04_wf1_Surface.B04_wf1_Surface_Engine import run_surface_analysis
|
|
from B04_wf1_Surface.B04_wf1_Surface_Repository import (
|
|
create_processed_point_cloud,
|
|
create_surface_model,
|
|
create_terrain_layer,
|
|
get_input_file,
|
|
list_surface_models,
|
|
)
|
|
from B04_wf1_Surface.B04_wf1_Surface_Schema import (
|
|
SurfaceAnalyzeRequest,
|
|
SurfaceAnalyzeResponse,
|
|
SurfaceModelListResponse,
|
|
SurfaceModelSummary,
|
|
)
|
|
from common_util.common_util_storage import resolve_stored_project_path
|
|
from config.config_db import get_db_pool
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/projects", tags=["B04 Surface Analysis"])
|
|
|
|
|
|
@router.post("/{project_id}/surface/analyze", response_model=SurfaceAnalyzeResponse)
|
|
async def analyze_surface(
|
|
project_id: UUID, request: SurfaceAnalyzeRequest
|
|
) -> SurfaceAnalyzeResponse | JSONResponse:
|
|
"""LAS 구조화·지면 필터·지표면 모델 생성을 실행하고 DB에 기록한다."""
|
|
try:
|
|
source_filters = request.resolved_filters()
|
|
methods = request.resolved_methods()
|
|
except ValueError as exc:
|
|
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
|
|
|
pool = get_db_pool()
|
|
try:
|
|
async with pool.acquire() as connection:
|
|
stored_path = await get_project_storage_relative_path(connection, project_id)
|
|
project_root = Path(resolve_stored_project_path(stored_path))
|
|
input_file = await get_input_file(connection, project_id, request.input_file_id)
|
|
las_path = project_root / Path(input_file["raw_file_path"])
|
|
if not las_path.is_file():
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={"status": "error", "message": "원본 LAS 파일을 찾을 수 없습니다."},
|
|
)
|
|
|
|
# 무거운 지형 연산은 이벤트 루프를 막지 않도록 별도 스레드에서 실행.
|
|
result = await asyncio.to_thread(
|
|
run_surface_analysis,
|
|
project_root,
|
|
las_path,
|
|
source_filters=source_filters,
|
|
methods=methods,
|
|
force=request.force,
|
|
)
|
|
|
|
# DB 기록 (트랜잭션)
|
|
await connection.begin()
|
|
try:
|
|
processed = result["processed"]
|
|
processed_cloud_id = await create_processed_point_cloud(
|
|
connection,
|
|
input_file_id=request.input_file_id,
|
|
project_id=project_id,
|
|
process_type="structured",
|
|
processed_file_path=processed["processed_file_path"],
|
|
converted_format=None,
|
|
converted_file_path=processed["converted_file_path"],
|
|
point_count=processed["point_count"],
|
|
bounds=processed["bounds"],
|
|
statistics=processed["statistics"],
|
|
classification_summary=None,
|
|
processing_params={"filters": source_filters},
|
|
)
|
|
surface_model_ids: list[int] = []
|
|
for model in result["models"]:
|
|
model_id = await create_surface_model(
|
|
connection,
|
|
project_id=project_id,
|
|
model_type=model["model_type"],
|
|
source_file_id=request.input_file_id,
|
|
processed_cloud_id=processed_cloud_id,
|
|
crs_epsg=input_file["crs_epsg"],
|
|
resolution_m=model["resolution_m"],
|
|
model_file_path=model["model_file_path"],
|
|
generation_params=model["generation_params"],
|
|
)
|
|
surface_model_ids.append(model_id)
|
|
for layer in model["layers"]:
|
|
await create_terrain_layer(
|
|
connection,
|
|
surface_model_id=model_id,
|
|
layer_name=layer["layer_name"],
|
|
geometry_type=layer["geometry_type"],
|
|
layer_file_path=layer["file_path"],
|
|
file_format=layer["file_format"],
|
|
file_size_mb=None,
|
|
statistics=None,
|
|
)
|
|
await connection.commit()
|
|
except Exception:
|
|
await connection.rollback()
|
|
raise
|
|
|
|
return SurfaceAnalyzeResponse(
|
|
project_id=str(project_id),
|
|
ground_summary=result["ground_summary"],
|
|
manifest_status=result["manifest"].get("status", "unknown"),
|
|
surface_model_ids=surface_model_ids,
|
|
)
|
|
except LookupError as exc:
|
|
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
|
|
except (OSError, ValueError) as exc:
|
|
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
|
except Exception:
|
|
logger.exception("B04 지표면 분석 실패: project_id=%s", project_id)
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"status": "error", "message": "지표면 분석 처리 중 오류가 발생했습니다."},
|
|
)
|
|
|
|
|
|
@router.get("/{project_id}/surface/models", response_model=SurfaceModelListResponse)
|
|
async def get_surface_models(project_id: UUID) -> SurfaceModelListResponse | JSONResponse:
|
|
"""프로젝트의 지표면 모델 목록을 조회한다."""
|
|
pool = get_db_pool()
|
|
try:
|
|
async with pool.acquire() as connection:
|
|
models = await list_surface_models(connection, project_id)
|
|
return SurfaceModelListResponse(
|
|
project_id=str(project_id),
|
|
models=[SurfaceModelSummary(**model) for model in models],
|
|
)
|
|
except Exception:
|
|
logger.exception("B04 지표면 모델 목록 조회 실패: project_id=%s", project_id)
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"status": "error", "message": "모델 목록 조회 중 오류가 발생했습니다."},
|
|
)
|