- 한 번에 이어 붙이는 도엽 수 36 장(6 × 6) 넘으면 413 + 알림 글(몇 장 · 줄이는 법) - 범위 보기 응답에 store_sheets · max_sheets - 시험 530_1 · 530_2 갱신 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
226 lines
11 KiB
Python
226 lines
11 KiB
Python
"""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
|