"""D 예정노선 지형 받기(PLAN 530-1) — 가짜 내려받기 · 옛 도엽 유지 · 새 도엽만 · 빠짐 없음. ./venv/Scripts/python.exe -m pytest resources/tester/test_530_1_plan_terrain.py -q """ import json import math from pathlib import Path import pytest from pyproj import Transformer from B04_PreProcess import B04_PreProcess_Engine_SheetStore as store_mod from B04_PreProcess.B04_PreProcess_Engine_MapSheet import sheets_for_points from common_util.common_util_crs import resolve_project_crs from config.config_system_terrain import SHEET_SURFACE_MARGIN_M from D03_PlanAnalysis import D03_PlanAnalysis_Terrain as terrain EPSG = 5186 A, B, C = (230000.0, 450000.0), (231500.0, 451200.0), (238000.0, 455000.0) @pytest.fixture def fake(tmp_path, monkeypatch): seen = {"download": [], "merge": [], "satellite": [], "dem": []} def download_sheet(store_dir, sheet_no, version=2): seen["download"].append(sheet_no) store_dir = Path(store_dir) store_dir.mkdir(parents=True, exist_ok=True) (store_dir / f"{sheet_no}.zip").write_bytes(b"PK") index = store_mod._load_index(store_dir) index["sheets"][sheet_no] = {"file": f"{sheet_no}.zip"} store_mod._save_index(store_dir, index) def merge(store_dir, sheets, output_dir, layers=None): seen["merge"].append(list(sheets)) (output_dir / "도엽_등고선.geojson").write_text("{}", encoding="utf-8") return {} def satellite(prj, bounds, output_dir, layer_name="Satellite", ext="jpeg"): seen["satellite"].append(bounds) meta = {"x_min": bounds["x"][0], "x_max": bounds["x"][1]} meta.update({"y_min": bounds["y"][0], "y_max": bounds["y"][1]}) (output_dir / "vworld_satellite_meta.json").write_text(json.dumps(meta), "utf-8") return output_dir / "vworld_satellite.png" def dem(root, processed, models_dir, route_xy, crs, methods=None): seen["dem"].append((models_dir.name, route_xy.tolist(), crs)) return [{"model_file_path": f"B04_PreProcess/{models_dir.name}/dtm.npz"}] monkeypatch.setattr(store_mod, "download_sheet", download_sheet) monkeypatch.setattr(terrain, "parse_and_merge_sheets", merge) monkeypatch.setattr(terrain, "download_vworld_satellite_map", satellite) monkeypatch.setattr(terrain, "build_sheet_surface_model", dem) return tmp_path, seen def _zips(root): return sorted(p.stem for p in store_mod.get_project_map_sheets_dir(root).glob("*.zip")) def test_more_points_keep_old_sheets_and_fetch_only_new(fake) -> None: root, seen = fake first = terrain.ensure_plan_terrain(root, [{"year": 2026, "start": A, "end": B}], EPSG) old = _zips(root) assert sorted(seen["download"]) == old == sorted(first["sheets"]["downloaded"]) assert first["satellite"] == "done" and list(first["years"]) == [2026] # 작업 좌표계 .prj — B 좌표계 창구가 읽음 assert resolve_project_crs(root) == f"EPSG:{EPSG}" seen["download"].clear() segs = [{"year": 2026, "start": A, "end": B}, {"year": 2027, "start": B, "end": C}] second = terrain.ensure_plan_terrain(root, segs, EPSG) now = _zips(root) assert set(old) <= set(now) # 옛 도엽 그대로(prune 없음) assert sorted(seen["download"]) == sorted(set(now) - set(old)) # 새 도엽만 받음 assert seen["merge"][-1] == now # 병합 = 옛 ∪ 새 # 위성 = 옛 범위 ∪ 새 범위 first_box, union = seen["satellite"][0], seen["satellite"][-1] assert union["x"][0] <= first_box["x"][0] and union["x"][1] >= first_box["x"][1] assert union["y"][0] <= first_box["y"][0] and union["y"][1] >= first_box["y"][1] assert [d[0] for d in seen["dem"][-2:]] == ["models_2026", "models_2027"] assert second["sheets"]["failed"] == {} # 같은 지점 다시 — 받기 0 · 위성 건너뜀 · 병합 열쇠 같음 seen["download"].clear() merges = len(seen["merge"]) third = terrain.ensure_plan_terrain(root, segs, EPSG) assert seen["download"] == [] and third["satellite"] == "skip" assert len(seen["merge"]) == merges def test_far_points_leave_no_gap(fake) -> None: root, _ = fake far = (A[0] + 9000.0, A[1] + 7000.0) # 11 km — 3×3 두 벌이 안 닿는 거리 got = terrain.ensure_plan_terrain(root, [{"year": 2026, "start": A, "end": far}], EPSG) to_wgs = Transformer.from_crs(f"EPSG:{EPSG}", "EPSG:4326", always_xy=True) on_line = [] for i in range(201): lon, lat = to_wgs.transform( A[0] + (far[0] - A[0]) * i / 200, A[1] + (far[1] - A[1]) * i / 200 ) on_line.append((lat, lon)) assert set(sheets_for_points(on_line)) <= set(got["sheets"]["merged"]) def test_dem_outline_covers_corridor() -> None: box = terrain.dem_outline(A, B) reach = terrain.PLAN_CORRIDOR_M + terrain.DEM_EXTRA_M assert box[0][0] - SHEET_SURFACE_MARGIN_M <= min(A[0], B[0]) - reach + 1e-9 assert box[1][1] + SHEET_SURFACE_MARGIN_M >= max(A[1], B[1]) + reach - 1e-9 samples = terrain.segment_samples(A, C) steps = [math.dist(p, q) for p, q in zip(samples, samples[1:])] assert samples[0] == A and samples[-1] == C and max(steps) <= terrain.SAMPLE_STEP_M