Files
Aislo/resources/tester/test_530_1_plan_terrain.py
T
eomsangdonandClaude Sonnet 5.5 db5a2f0408 feat(D 예정노선): 받기 상한 = 칸 500 만 → 공용 창고 도엽 36 장 (PLAN 613-1 · 요구 493)
- 한 번에 이어 붙이는 도엽 수 36 장(6 × 6) 넘으면 413 + 알림 글(몇 장 · 줄이는 법)
- 범위 보기 응답에 store_sheets · max_sheets
- 시험 530_1 · 530_2 갱신

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 23:11:41 +09:00

226 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""D 예정노선 지형 받기(PLAN 530-1 · 561-2 · 595) — 사업 전체 한 벌 · 범위 보기 = 내려받기 없음 ·
옛 도엽 유지 · 새 도엽만 · 범위는 줄지 않음 · 빠짐 없음 · 지적도 뺌 · 새로(reset) = 옛 범위 버림 ·
받은 범위 좌표계 · 칸 상한 · 거리 · DEM = 공용 지형 창고 이어 붙임(608-1 · 값 같으면 안 다시 씀).
./venv/Scripts/python.exe -m pytest resources/tester/test_530_1_plan_terrain.py -q
"""
import json
from pathlib import Path
import numpy as np
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": [], "warm": []}
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 load_terrain(epsg, *, sheets=None, box=None, cell_m=2.0, on_progress=None):
seen["dem"].append(list(box))
seen["grid_m"] = cell_m
on_progress(50, "지형 계산")
x = (np.arange(10) + np.floor(box[0] / cell_m) + 0.5) * cell_m # 작은 격자 · 원점 칸 배수
y = (np.arange(8) + np.floor(box[1] / cell_m) + 0.5) * cell_m
z = seen["z"] + 0.1 * np.arange(80, dtype=np.float32).reshape(8, 10)
grid = {"x": x, "y": y, "z": z, "valid_mask": np.isfinite(z), "cell_m": cell_m}
return grid | {"epsg": epsg, "sheets": ["1"], "computed": [], "failed": {}}
seen["z"] = np.float32(100.0)
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, "load_terrain", load_terrain)
monkeypatch.setattr(terrain, "_warm_store", lambda *a: seen["warm"].append(a[0]) or [])
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 _dem_box(seen) -> list[float]:
"""창고에 물은 상자 = 받은 범위 그대로."""
return seen["dem"][-1]
def test_one_set_and_extend_keeps_old(fake) -> None:
root, seen = fake
first = terrain.ensure_plan_terrain(root, [A, B], EPSG)
old = _zips(root)
assert sorted(seen["download"]) == old == sorted(first["sheets"]["downloaded"])
assert first["satellite"] == "done" and len(first["models"]) == 1
model = first["models"][0]["model_file_path"]
assert model == "B04_PreProcess/models_plan/dtm_sheet_laplace.npz" # 연도 없는 한 벌
with np.load(root / model) as dem: # 뒤 소비자 꼴 — y 오름차순 · 칸 2 m
assert dem["z"].shape == (8, 10) and dem["y"][1] > dem["y"][0]
assert float(dem["resolution"][0]) == 2.0 and dem["valid_mask"].all()
assert dem["z"][1, 0] == pytest.approx(101.0) # 창고 값 그대로(뒤집힘 없음)
assert (root / model.replace(".npz", "_smooth.npz")).is_file()
m = terrain.DEFAULT_MARGIN_M
box = [min(A[0], B[0]) - m, min(A[1], B[1]) - m, max(A[0], B[0]) + m, max(A[1], B[1]) + m]
assert _dem_box(seen) == pytest.approx(box) == first["box_m"]
assert seen["warm"][-1] == pytest.approx(box) # 새 도엽 함께 셈 = 같은 상자
assert terrain.received_box(root) == pytest.approx(tuple(box))
# 작업 좌표계 .prj — B 좌표계 창구가 읽음
assert resolve_project_crs(root) == f"EPSG:{EPSG}"
# 그은 점이 범위 밖 — 더 받기 = 옛 범위 ∪ 새 범위 · 새 도엽만
seen["download"].clear()
second = terrain.ensure_plan_terrain(root, [A, B, C], EPSG)
now = _zips(root)
assert set(old) <= set(now) # 옛 도엽 그대로(prune 없음)
assert sorted(seen["download"]) == sorted(set(now) - set(old))
assert seen["merge"][-1] == now # 병합 = 옛 ∪ 새
grown = second["box_m"]
assert grown[0] <= box[0] and grown[1] <= box[1] and grown[2] >= C[0] + m
first_sat, union = seen["satellite"][0], seen["satellite"][-1]
assert union["x"][0] <= first_sat["x"][0] and union["x"][1] >= first_sat["x"][1]
assert second["sheets"]["failed"] == {}
# 같은 범위 다시 — 받기 0 · 위성 건너뜀 · 병합 열쇠 같음 · 작은 점이어도 범위 줄지 않음
seen["download"].clear()
merges = len(seen["merge"])
third = terrain.ensure_plan_terrain(root, [A, B], EPSG)
assert seen["download"] == [] and third["satellite"] == "skip"
assert len(seen["merge"]) == merges and third["box_m"] == grown
# 창고 값 같음 = DEM 안 다시 씀 · 값 바뀜(창고 다시 셈) = 다시 씀
npz = root / third["models"][0]["model_file_path"]
stamp = npz.stat().st_mtime_ns
terrain.ensure_plan_terrain(root, [A, B], EPSG)
assert npz.stat().st_mtime_ns == stamp
seen["z"] = np.float32(200.0)
terrain.ensure_plan_terrain(root, [A, B], EPSG)
with np.load(npz) as dem:
assert float(dem["z"][0, 0]) == 200.0
def test_reset_drops_old_box_sheets_satellite(fake) -> None:
"""595-2 — 새로 = 옛 받은 범위 · 병합 도엽 · 위성 범위 버림 · 좌표계 · 가운데 기록 ·
다른 좌표계로 물으면 받은 범위 없음 · 지적도는 지형 일에 없음."""
root, seen = fake
terrain.ensure_plan_terrain(root, [A, B], EPSG, center=(127.5, 36.0))
old = set(seen["merge"][-1])
far = [(A[0] + 40000.0, A[1]), (A[0] + 41000.0, A[1] + 1000.0)]
got = terrain.ensure_plan_terrain(root, far, EPSG, reset=True, center=(128.0, 36.0))
m = terrain.DEFAULT_MARGIN_M
assert got["box_m"] == pytest.approx(
[far[0][0] - m, far[0][1] - m, far[1][0] + m, far[1][1] + m]
)
assert not old & set(seen["merge"][-1]) # 옛 도엽 병합에서 빠짐(zip 은 남음)
assert seen["satellite"][-1]["x"][0] > A[0] + 30000 # 옛 위성 범위와 안 합침
assert "cadastral" not in got
meta = terrain.received_meta(root)
assert meta["epsg"] == EPSG and meta["center"] == [128.0, 36.0]
assert terrain.received_box(root, EPSG) == pytest.approx(tuple(got["box_m"]))
assert terrain.received_box(root, 5187) is None # 다른 띠 = 받은 범위 아님
# 새로 아닌 이어 받기 = 가운데 기록 그대로
terrain.ensure_plan_terrain(root, far, EPSG)
assert terrain.received_meta(root)["center"] == [128.0, 36.0]
def test_cells_distance_and_limit(fake) -> None:
root, _ = fake
assert terrain.cells_of((0.0, 0.0, 2000.0, 2000.0)) == 1_000_000 # 2 m 격자
assert terrain.distance_to_box((5.0, 5.0), (0.0, 0.0, 10.0, 10.0)) == 0
assert terrain.distance_to_box((13.0, 14.0), (0.0, 0.0, 10.0, 10.0)) == 5
assert terrain.MAX_SHEETS == 36 and terrain.FAR_M == 5000
view = terrain.plan_extent(root, [A, B], EPSG)
assert view["max_sheets"] == terrain.MAX_SHEETS and "max_cells" not in view
assert view["store_sheets"] == terrain.store_sheet_count(tuple(view["box_m"]), EPSG)
def test_sheet_limit_counts_store_sheets(fake) -> None:
"""613-1 — 상한 = 공용 창고 도엽 수(칸 수 아님) · 36 장까지 · 넘으면 알림 글에 장 수."""
root, _ = fake
small = terrain.plan_extent(root, [A, B], EPSG)
assert 1 <= small["store_sheets"] <= 4 and small["cells"] < 2_000_000
# 남쪽 1.5 km 늘림 — 옛 칸 상한(500 만 칸)은 넘지만 도엽은 상한 안
south = (A[0], A[1] - 6000.0)
wide = terrain.plan_extent(root, [A, B, south], EPSG)
assert wide["store_sheets"] <= terrain.MAX_SHEETS
# 37 장 이상 — 약 6 도엽 폭 넘게 벌림
huge = terrain.plan_extent(root, [A, (A[0] + 20000.0, A[1] + 22000.0)], EPSG)
assert huge["store_sheets"] > terrain.MAX_SHEETS
text = terrain.sheet_limit_message(huge["store_sheets"])
assert (
f"도엽 {huge['store_sheets']}장" in text and "36장까지" in text and "새로 받으세요" in text
)
def test_extent_no_download_and_covered(fake) -> None:
root, seen = fake
before = terrain.plan_extent(root, [A, B], EPSG)
assert seen["download"] == [] and before["have"] is None and before["covered"] is False
assert before["margin_m"] == terrain.DEFAULT_MARGIN_M and len(before["box"]) == 4
x0, y0, x1, y1 = before["box_m"]
assert before["cells"] == int((x1 - x0) * (y1 - y0) / 4) # D 지형 격자 2 m
assert all(not s["stored"] and len(s["ring"]) == 4 for s in before["sheets"])
terrain.ensure_plan_terrain(root, [A, B], EPSG)
assert seen["grid_m"] == 2.0 # B 공용 1 m 가 아니라 D 전용 값
after = terrain.plan_extent(root, [A, B], EPSG)
assert after["covered"] and after["have_m"] == after["box_m"]
assert all(s["stored"] for s in after["sheets"])
assert {s["no"] for s in after["sheets"]} == {s["no"] for s in before["sheets"]}
assert not terrain.plan_extent(root, [A, C], EPSG)["covered"] # 그은 점이 밖 = 더 받기
# 여유 — 바닥 B 절취 여유 · 빈 값 = 처음 값
assert terrain.clamp_margin(100.0) == SHEET_SURFACE_MARGIN_M
assert terrain.clamp_margin(None) == terrain.DEFAULT_MARGIN_M
assert terrain.clamp_margin(1e9) == terrain.MAX_MARGIN_M
def test_box_sheets_leave_no_gap(fake) -> None:
root, _ = fake
far = (A[0] + 9000.0, A[1] + 7000.0) # 11 km
got = terrain.ensure_plan_terrain(root, [A, far], EPSG)
x0, y0, x1, y1 = got["box_m"]
to_wgs = Transformer.from_crs(f"EPSG:{EPSG}", "EPSG:4326", always_xy=True)
inside = []
for i in range(41):
for j in range(41):
lon, lat = to_wgs.transform(x0 + (x1 - x0) * i / 40, y0 + (y1 - y0) * j / 40)
inside.append((lat, lon))
assert set(sheets_for_points(inside)) <= set(got["sheets"]["merged"])
def test_empty_surface_raises_and_keeps_no_box(fake, monkeypatch) -> None:
root, _ = fake
seen = fake[1]
seen["z"] = np.float32(np.nan) # 창고에 등고선 없음(빈 도엽)
with pytest.raises(ValueError, match="사업 지형면을 만들지 못했습니다"):
terrain.ensure_plan_terrain(root, [A, B], EPSG)
assert terrain.received_box(root) is None