"""B04 지표면 분석 엔진 오케스트레이터. 원본 LAS를 구조화하고 지면 필터를 실행한 뒤 지표면 5종 모델을 빌드하는 동기 계산 파이프라인. 라우터에서 asyncio.to_thread로 호출한다. """ from collections.abc import Callable from pathlib import Path from typing import Any import numpy as np from B04_wf1_Surface.B04_wf1_Surface_Engine_Ground import build_ground_masks, summarize_masks from B04_wf1_Surface.B04_wf1_Surface_Engine_Pipeline import build_all_terrain_models from B04_wf1_Surface.B04_wf1_Surface_Engine_Structurize import structurize_las from config.config_system import build_surface_model_config # 진행 콜백 시그니처: (진행률 0~100, 현재 단계 키, 메시지) ProgressCallback = Callable[[int, str, str], None] def _relative_to_project(project_root: Path, path: Path) -> str: """프로젝트 루트 기준 posix 상대 경로 문자열.""" return path.relative_to(project_root).as_posix() def run_surface_analysis( project_root: Path, las_path: Path, *, source_filters: list[str], methods: list[str], force: bool = False, on_progress: ProgressCallback | None = None, ) -> dict[str, Any]: """구조화→필터→모델 빌드를 수행하고 산출 메타데이터를 반환한다. 반환 dict: - processed: {processed_file_path, converted_file_path, point_count, bounds, statistics} - ground_summary: 필터별 지면 포인트 요약 - manifest: 지표면 모델 파이프라인 manifest - models: [{model_type, model_file_path, resolution_m, generation_params, layers}] """ def _report(percent: int, stage: str, message: str) -> None: if on_progress is not None: on_progress(percent, stage, message) stage_root = project_root / "B04_wf1_Surface" processed_dir = stage_root / "processed" models_dir = stage_root / "models" processed_dir.mkdir(parents=True, exist_ok=True) models_dir.mkdir(parents=True, exist_ok=True) # 1. LAS 구조화 (structured.npz) _report(10, "structurize", "LAS 구조화 중") structured_path = structurize_las(las_path, processed_dir) with np.load(structured_path) as structured: xyz = structured["xyz"] bounds = structured["bounds"] total_points = int(len(xyz)) stats = { "min_z": float(bounds[2, 0]), "max_z": float(bounds[2, 1]), "mean_z": float(np.mean(xyz[:, 2])) if total_points else None, } bounds_dict = { "x_min": float(bounds[0, 0]), "x_max": float(bounds[0, 1]), "y_min": float(bounds[1, 0]), "y_max": float(bounds[1, 1]), } data = {"xyz": xyz, "bounds": bounds} # 2. 지면 필터 실행 _report(40, "ground_filter", "지면 필터 적용 중") masks = build_ground_masks(data, source_filters) ground_summary = summarize_masks(data, masks) # 3. 지표면 5종 모델 빌드 _report(70, "surface_model", "지표면 모델 생성 중") config = build_surface_model_config() config["source_filters"] = list(source_filters) config["precompute"] = list(methods) manifest = build_all_terrain_models(data, masks, models_dir, config, force=force) # 3-2. VWorld 지도 및 국가 GIS 벡터 다운로드 _report(90, "download_maps", "VWorld 지도 및 GIS 벡터 데이터 다운로드 중") try: # project_root 내의 .prj 파일 탐색 (B03_FileInput/input 또는 root 내 존재할 수 있음) prj_files = ( list(project_root.glob("**/B03_FileInput/input/*.prj")) + list(project_root.glob("*.prj")) + list(project_root.glob("**/*.prj")) ) prj_path = prj_files[0] if prj_files else project_root / "result.prj" bounds_dict_for_download = { "x": [float(bounds[0, 0]), float(bounds[0, 1])], "y": [float(bounds[1, 0]), float(bounds[1, 1])], "z": [float(bounds[2, 0]), float(bounds[2, 1])], } from B04_wf1_Surface.B04_wf1_Surface_Engine_GisVector import download_all_gis_vectors from B04_wf1_Surface.B04_wf1_Surface_Engine_VWorld import download_vworld_satellite_map # VWorld 지도 및 GIS 데이터 저장 위치는 B04_wf1_Surface/processed에 보관. layers = [ {"layer": "Satellite", "ext": "jpeg"}, {"layer": "Hybrid", "ext": "png"}, {"layer": "white", "ext": "png"}, ] for item in layers: try: download_vworld_satellite_map( prj_path, bounds_dict_for_download, processed_dir, layer_name=item["layer"], ext=item["ext"], ) except Exception: pass try: download_all_gis_vectors(prj_path, bounds_dict_for_download, processed_dir) except Exception: pass except Exception: pass _report(95, "saving", "결과 저장 중") processed = { "processed_file_path": _relative_to_project(project_root, structured_path), "converted_file_path": None, "point_count": total_points, "bounds": bounds_dict, "statistics": stats, } # manifest에서 저장된 모델별 정보 추출 (필터별 대표 모델) models: list[dict[str, Any]] = [] for filter_key, filter_entry in manifest.get("source_filters", {}).items(): for method, meta in filter_entry.get("methods", {}).items(): if meta.get("status") != "completed": continue model_file = meta.get("model_file") model_path = (models_dir / model_file) if model_file else None layers: list[dict[str, Any]] = [] if meta.get("preview_file"): layers.append( { "layer_name": f"{method}_{filter_key}_preview", "geometry_type": "MESH" if method != "meshfree" else "POINTCLOUD", "file_path": _relative_to_project( project_root, models_dir / meta["preview_file"] ), "file_format": "glb" if method != "meshfree" else "ply", } ) models.append( { "model_type": method, "source_filter": filter_key, "representation": meta.get("representation"), "model_file_path": _relative_to_project(project_root, model_path) if model_path else None, "resolution_m": meta.get("grid_resolution_meters"), "generation_params": { "source_filter": filter_key, "representation": meta.get("representation"), "footprint_area_m2": meta.get("footprint_area_m2"), }, "layers": layers, } ) return { "processed": processed, "ground_summary": ground_summary, "manifest": manifest, "models": models, }