"""PLAN 624-4 — D 예정노선 라이다: LAS 올리기 → B04 엔진 지형 → `terrain=lidar` 겹(요구 530 · 531). D 몫 길 = `D03_PlanAnalysis_Lidar` · B04 엔진은 `maps=False` 인자만 덧(기본 = B 그대로). """ from __future__ import annotations from pathlib import Path import numpy as np import pytest from fastapi import HTTPException from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers from D03_PlanAnalysis import D03_PlanAnalysis_Lidar as lidar from D03_PlanAnalysis import D03_PlanAnalysis_TerrainSource as source D_EPSG, LAS_EPSG = 5187, 5176 # D = GRS80 동부 · 업체 LAS = 베셀 동부(검증 용화와 같음) def _write_las(path: Path, epsg: int | None, x0: float, y0: float) -> None: """두 덩이(가운데 빈 땅) 지면점 LAS — 비탈 0.3.""" import laspy from pyproj import CRS rng = np.random.default_rng(7) xs, ys = [], [] for dx in (0.0, 160.0): xs.append(rng.uniform(x0 + dx, x0 + dx + 60, 6000)) ys.append(rng.uniform(y0, y0 + 60, 6000)) x, y = np.concatenate(xs), np.concatenate(ys) header = laspy.LasHeader(point_format=3, version="1.2") header.offsets = [round(x0), round(y0), 0] header.scales = [0.01, 0.01, 0.01] if epsg is not None: header.add_crs(CRS.from_epsg(epsg)) data = laspy.LasData(header) data.x, data.y, data.z = x, y, 500 + 0.3 * (x - x0) data.classification = np.full(len(x), 2, np.uint8) path.parent.mkdir(parents=True, exist_ok=True) data.write(path) def _built(tmp_path: Path, monkeypatch, epsg: int | None = LAS_EPSG) -> dict: # type: ignore[no-untyped-def] import B04_PreProcess.B04_PreProcess_Engine as engine called: list[int] = [] monkeypatch.setattr(engine, "download_geodata", lambda *a, **k: called.append(1)) _write_las(lidar.lidar_root(tmp_path) / lidar.UPLOAD / "t.las", epsg, 208300.0, 367500.0) got = lidar.build_lidar(tmp_path, D_EPSG, lambda *_: None) assert called == [] # 지도 · GIS 받기 안 함 return got def test_이름_고르기() -> None: lidar.check_names(["a.las", "a.prj", "b.LAZ"]) for names in (["a.prj"], ["a.las", "a.txt"], ["a.las", "A.las"]): with pytest.raises(HTTPException) as caught: lidar.check_names(names) assert caught.value.status_code == 400 def test_올리기_뒤_라이다_지형_B_꼴(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def] got = _built(tmp_path, monkeypatch) meta = got["meta"] dem = lidar.lidar_dem(tmp_path) assert dem is not None and dem.name == f"dtm_{meta['source_filter']}.npz" assert dem.parent == lidar.lidar_root(tmp_path) / "B04_PreProcess" / "models" assert meta["files"] == ["t.las"] and meta["point_count"] == 12000 assert not (lidar.lidar_root(tmp_path) / lidar.UPLOAD).exists() # surface_models 행 경로 = 프로젝트 뿌리 기준 from B04_PreProcess.B04_PreProcess_Repository import _validate_stage_path assert any(m["model_file_path"] == meta["model_file"] for m in got["models"]) assert all(_validate_stage_path(m["model_file_path"]) for m in got["models"]) # 좌표계 옮김 — 베셀 → GRS80 북 +10 만 m 언저리 from pyproj import Transformer want = Transformer.from_crs(LAS_EPSG, D_EPSG, always_xy=True).transform(208300.0, 367500.0) assert abs(meta["bounds"]["x_min"] - want[0]) < 1.0 assert abs(meta["bounds"]["y_min"] - want[1]) < 1.0 # 다시 올리면 갈음 _write_las(lidar.lidar_root(tmp_path) / lidar.UPLOAD / "u.las", None, 208300.0, 467500.0) again = lidar.build_lidar(tmp_path, D_EPSG, lambda *_: None)["meta"] assert again["files"] == ["u.las"] and abs(again["bounds"]["y_min"] - 467500.0) < 1.0 def test_terrain_lidar_급경사_0차곡_범위_밖_빈칸(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def] with pytest.raises(HTTPException) as caught: # 안 올림 = 404 source.need_dem(source.plan_frame(tmp_path, D_EPSG, (0, 0, 1, 1), "lidar")) assert caught.value.status_code == 404 _built(tmp_path, monkeypatch) frame = source.plan_frame(tmp_path, D_EPSG, (0, 0, 1, 1), "lidar") npz = source.need_dem(frame) assert frame.source == "lidar" and npz == lidar.lidar_dem(tmp_path) value = layers.slope_layer(tmp_path, npz.parent, D_EPSG, "slope", npz) counts = value["counts"] assert sum(counts) > 0 and max(range(len(counts)), key=counts.__getitem__) in (29, 30) # 두 덩이 사이 = 빈칸(수치지형도로 안 메움) with np.load(npz) as dem: valid = np.asarray(dem["valid_mask"], bool) x = np.asarray(dem["x"], float) x0 = float(x[0]) gap = (x > x0 + 90) & (x < x0 + 130) assert gap.any() and not valid[:, gap].any() work, light, full = source.zero_order_calls(frame, 50.0, 5000.0) assert work(*light) is None # 저장본 없음 value = work(*full) assert value["cell_m"] > 0 and work(*light) == value # 다음 = 저장본 def test_B04_기본은_지도_받기_그대로(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def] """B 프로젝트 전처리(기본 인자) = 지도 · GIS 받기 · 도엽 서피스 그대로 부름.""" import B04_PreProcess.B04_PreProcess_Engine as engine import B04_PreProcess.B04_PreProcess_Engine_SheetSurface as sheet calls: list[str] = [] monkeypatch.setattr(engine, "download_geodata", lambda *a, **k: calls.append("maps")) monkeypatch.setattr( sheet, "build_sheet_surface_from_route", lambda *a, **k: calls.append("sheet") or [] ) las = tmp_path / "B03_FileInput" / "input" / "las" / "t.las" _write_las(las, None, 200000.0, 500000.0) engine.run_surface_analysis(tmp_path, las, source_filters=None, methods=["dtm"]) assert calls == ["maps", "sheet"] calls.clear() engine.run_surface_analysis(tmp_path, las, source_filters=None, methods=["dtm"], maps=False) assert calls == []