"""D 예정노선 라이다 — LAS 올리기 → B04 엔진 그대로 지형(요구 530 · 531 · PLAN 624-4). - `POST /api/projects/{id}/plan/lidar` — LAS(+ .prj) 올리기 · 뒤에서 지형 짓기 (진행 = `/plan/progress` 의 `lidar`) · 다시 올리면 옛 LAS 를 갈음 - `GET …/plan/lidar` — 지은 라이다 지형(없으면 `ready: false`) 자리 = `B04_PreProcess/lidar/`(D 몫 · 안은 B 와 같은 꼴 — `B03_FileInput/input/las/` · `B04_PreProcess/models/`). LAS 좌표계(.prj · LAS 머리 · 없으면 프로젝트 좌표계)가 프로젝트와 다르면 옮겨 적음(높이 그대로). 지도 · GIS 받기 · 도엽 서피스는 안 함(`maps=False`) · 지은 DTM = `surface_models` 행 (높이 표본 격자). 라이다 범위 밖 = 빈칸(격자 `valid_mask`) · 수치지형도로 메우지 않음(결정 A⑥). 이름 검사 · 좌표계 · 옮겨 놓기 · 받기 · 짓기 = 공용 `common_util_lidar_upload`(B05 와 같이 · 637-1). 계산 자리 = ③ 서버 단독. """ from __future__ import annotations import asyncio import json from pathlib import Path from typing import Any, Callable from uuid import UUID from fastapi import APIRouter, File, HTTPException, UploadFile from common_util import common_util_lidar_upload as lidar_upload from common_util.common_util_json import atomic_write_json from common_util.common_util_lidar_upload import ( # noqa: F401 — 옛 이름(시험 · 부르는 쪽) LAS_SUFFIXES, check_names, las_epsg, place_las, ) router = APIRouter(prefix="/api/projects", tags=["D03 Plan Lidar"]) LIDAR = Path("B04_PreProcess") / "lidar" # 표면 모델 행 경로 = B04_PreProcess 아래 UPLOAD = "upload" LAS_DIR = Path("B03_FileInput") / "input" / "las" MODELS = Path("B04_PreProcess") / "models" META = "lidar_meta.json" def lidar_root(root: Path) -> Path: return Path(root) / LIDAR def read_meta(root: Path) -> dict[str, Any]: try: return json.loads((lidar_root(root) / META).read_text(encoding="utf-8")) except (OSError, ValueError): return {} def lidar_dem(root: Path) -> Path | None: """지은 라이다 DTM(평활 안 함) — 없으면 None.""" name = read_meta(root).get("model_file") path = Path(root) / name if name else None return path if path is not None and path.is_file() else None def build_lidar(root: Path, epsg: int, report: Callable[[int, str], None]) -> dict[str, Any]: """올린 자리(`upload/`)의 LAS → 입력 자리 갈음 → B04 엔진(지면 필터 · DTM) → 메타.""" base = lidar_root(root) las_paths = lidar_upload.stage_las(base / UPLOAD, base / LAS_DIR, epsg, report) result = lidar_upload.build_surface(base, las_paths, report) prefix = LIDAR.as_posix() models = [ {**model, "model_file_path": f"{prefix}/{model['model_file_path']}"} for model in result["models"] if model.get("model_file_path") ] picked = result["source_filters"][0] meta = { "epsg": int(epsg), "files": [p.name for p in las_paths], "source_filter": picked, "model_file": f"{prefix}/{(MODELS / f'dtm_{picked}.npz').as_posix()}", "point_count": result["processed"]["point_count"], "bounds": result["processed"]["bounds"], } atomic_write_json(base / META, meta) return {"meta": meta, "models": models} @router.post("/{project_id}/plan/lidar", response_model=None) async def upload_plan_lidar(project_id: UUID, files: list[UploadFile] = File(...)) -> dict: """D 에 LAS 올리기 → 뒤에서 라이다 지형(진행 = `/plan/progress` 의 `lidar`).""" from D03_PlanAnalysis import D03_PlanAnalysis_Router as base check_names([upload.filename or "" for upload in files]) root, epsg, _, _ = await base._plan_ends(project_id) # 예정노선 아님 400 if (base._JOBS.get((str(project_id), "lidar")) or {}).get("running"): raise HTTPException(status_code=409, detail="이미 돌고 있습니다.") await lidar_upload.receive_uploads(files, lidar_root(root) / UPLOAD) async def work(report: Any) -> None: got = await asyncio.to_thread(build_lidar, root, epsg, report) await base._save_models(project_id, got["models"], epsg) report(100, f"라이다 지형 끝 — 점 {got['meta']['point_count']:,}개") return base._start_job(project_id, "lidar", work, root) @router.get("/{project_id}/plan/lidar", response_model=None) async def plan_lidar(project_id: UUID) -> dict[str, Any]: """지은 라이다 지형 — `ready` · 메타(좌표계 · 파일 · 필터 · 점 수 · 범위).""" from D03_PlanAnalysis import D03_PlanAnalysis_Store as store root, _ = await store.load_plan(project_id) meta = await asyncio.to_thread(read_meta, root) ready = await asyncio.to_thread(lidar_dem, root) is not None return {"status": "success", "ready": ready, **meta}