from __future__ import annotations import json from datetime import datetime from pathlib import Path from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi import Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from .analyzer import ( LAS_PREVIEW_PATH, PREVIEW_PATH, ROOT_DIR, SAMPLE_DIR, analyze_project, analyze_sample_project, collect_project_files, confirm_sample_release, create_ground_filter_cache, find_las_in_dir, get_project_source_dir, get_project_storage_dir, load_analysis, load_analysis_for_project, load_or_create_las_points_for_project, load_or_create_las_points_sample, ) from .classifier import ClassifyParams, run_classification app = FastAPI(title="Forest Road Phase 1 Analyzer", version="0.1.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) FRONTEND_DIST = ROOT_DIR / "frontend" / "dist" FRONTEND_ASSETS = FRONTEND_DIST / "assets" if FRONTEND_ASSETS.exists(): app.mount("/assets", StaticFiles(directory=FRONTEND_ASSETS), name="frontend-assets") # ---- Health / Root ---- @app.get("/health") def health() -> dict[str, str]: return {"status": "ok"} @app.get("/", response_model=None) def root() -> FileResponse | dict[str, object]: index_path = FRONTEND_DIST / "index.html" if index_path.exists(): return FileResponse(index_path) return { "name": "Forest Road Phase 1 Analyzer", "status": "running", "frontend_dev": "http://localhost:5173", } @app.get("/favicon.ico", include_in_schema=False) def favicon() -> Response: return Response(status_code=204) # ---- Upload & Sample Check ---- class CheckSampleRequest(BaseModel): filenames: list[str] @app.post("/api/check-sample") def check_sample(body: CheckSampleRequest) -> dict[str, object]: sample_filenames = {f.name for f in SAMPLE_DIR.iterdir() if f.is_file()} matched = all(fn in sample_filenames for fn in body.filenames) return {"matched": matched, "project_id": "sample" if matched else None} @app.post("/api/upload") async def upload_files( las_file: UploadFile = File(...), prj_file: UploadFile = File(...), tfw_file: UploadFile = File(...), tif_file: UploadFile | None = File(None), ) -> dict[str, object]: project_id = f"upload_{datetime.now().strftime('%Y%m%d%H%M%S')}" raw_dir = ROOT_DIR / "storage" / "projects" / project_id / "raw" raw_dir.mkdir(parents=True, exist_ok=True) for upload in [las_file, prj_file, tfw_file] + ([tif_file] if tif_file else []): if upload and upload.filename: dest = raw_dir / upload.filename content = await upload.read() dest.write_bytes(content) result = analyze_project(project_id) return {"project_id": project_id, "status": result.get("status", "ready")} # ---- Dynamic project endpoints ---- @app.get("/api/projects/{project_id}/analysis") def get_project_analysis(project_id: str) -> dict[str, object]: analysis = load_analysis_for_project(project_id) if analysis is None: if project_id == "sample": return analyze_sample_project() return analyze_project(project_id) return analysis @app.get("/api/projects/{project_id}/las-points-sample") def get_project_las_points(project_id: str) -> dict[str, object]: return load_or_create_las_points_for_project(project_id) @app.post("/api/projects/{project_id}/analyze-ground") def run_ground_analysis(project_id: str) -> dict[str, object]: storage_dir = get_project_storage_dir(project_id) stats_path = storage_dir / "processed" / "ground-stats.json" if stats_path.exists(): return json.loads(stats_path.read_text(encoding="utf-8")) result = create_ground_filter_cache(project_id) stats_path.parent.mkdir(parents=True, exist_ok=True) stats_path.write_text(json.dumps(result, ensure_ascii=False), encoding="utf-8") return result class ClassifyRequest(BaseModel): rgb_exg_threshold: float = 0.05 ground_height_threshold: float = 1.5 @app.post("/api/projects/{project_id}/classify") def classify_points(project_id: str, body: ClassifyRequest) -> dict[str, object]: source_dir = get_project_source_dir(project_id) las_path = find_las_in_dir(source_dir) if not las_path.exists(): raise HTTPException(status_code=404, detail="LAS 파일을 찾을 수 없습니다.") cache_path = get_project_storage_dir(project_id) / "processed" / "features.npz" params = ClassifyParams( rgb_exg_threshold=body.rgb_exg_threshold, ground_height_threshold=body.ground_height_threshold, ) return run_classification(las_path, cache_path, params) @app.get("/api/projects/{project_id}/all-points") def get_all_points(project_id: str) -> dict[str, object]: path = get_project_storage_dir(project_id) / "processed" / "all-points.json" if not path.exists(): raise HTTPException(status_code=404, detail="전체 포인트 캐시가 없습니다. analyze-ground를 먼저 실행하세요.") return json.loads(path.read_text(encoding="utf-8")) @app.get("/api/projects/{project_id}/ground-points") def get_ground_points(project_id: str) -> dict[str, object]: path = get_project_storage_dir(project_id) / "processed" / "ground-points.json" if not path.exists(): raise HTTPException(status_code=404, detail="지면 포인트 캐시가 없습니다. analyze-ground를 먼저 실행하세요.") return json.loads(path.read_text(encoding="utf-8")) @app.get("/api/projects/{project_id}/preview") def get_project_preview(project_id: str) -> FileResponse: preview_path = get_project_storage_dir(project_id) / "processed" / "preview.png" if not preview_path.exists(): if project_id == "sample": if not PREVIEW_PATH.exists(): analyze_sample_project() preview_path = PREVIEW_PATH else: raise HTTPException(status_code=404, detail="미리보기 이미지가 없습니다.") if not preview_path.exists(): raise HTTPException(status_code=404, detail="미리보기 이미지를 생성하지 못했습니다.") return FileResponse(preview_path, media_type="image/png") # ---- Legacy sample endpoints (backward compat) ---- @app.get("/api/projects/sample") def get_sample_project() -> dict[str, str]: return { "id": "sample", "name": "개발용 샘플 프로젝트", "description": "samples/step1_scan 기반 Phase 1 분석 대상", } @app.get("/api/projects/sample/files") def get_sample_files() -> dict[str, object]: return {"project_id": "sample", "files": collect_project_files()} @app.post("/api/projects/sample/analyze") def run_sample_analysis() -> dict[str, object]: return analyze_sample_project() @app.get("/api/projects/sample/las-preview") def get_sample_las_preview() -> FileResponse: if not LAS_PREVIEW_PATH.exists(): analyze_sample_project() if not LAS_PREVIEW_PATH.exists(): raise HTTPException(status_code=404, detail="LAS preview image is not available.") return FileResponse(Path(LAS_PREVIEW_PATH), media_type="image/png") @app.post("/api/projects/sample/confirm") def confirm_sample() -> dict[str, object]: return confirm_sample_release()