"""등고선 줌별 벡터 조각(PLAN 615) — MVT 풀어 읽기(겹 `contour` · `h` · `k`) · 줌별 간격 · 빈 조각(바다 · 국내 밖) = 빈 MVT · 처음만 만들고 둘째 = 파일 · 조각 번호 범위 · 화면 = 고정 벡터 소스. ./venv/Scripts/python.exe -m pytest resources/tester/test_615_contour_tiles.py -q """ import math from pathlib import Path import mapbox_vector_tile import pytest from B04_PreProcess.B04_PreProcess_Engine_MapSheet import sheet5k_center from common_util import common_util_contour_tiles as ct ROOT = Path(__file__).resolve().parents[2] SHEET = "36901074" # 상주 용화 산지 needs_gpkg = pytest.mark.skipif(not ct.NATIONAL_GPKG.is_file(), reason="전국 등고선 파일 없음") def _tile(lon: float, lat: float, z: int) -> tuple[int, int, int]: n = 2**z r = math.radians(lat) y = (1 - math.log(math.tan(r) + 1 / math.cos(r)) / math.pi) / 2 * n return z, int((lon + 180) / 360 * n), int(y) def _features(data: bytes) -> list[dict]: return mapbox_vector_tile.decode(data).get(ct.LAYER, {"features": []})["features"] @needs_gpkg @pytest.mark.parametrize("z", [12, 13, 14, 15]) def test_zoom_step_and_bold(z: int) -> None: lat, lon = sheet5k_center(SHEET) features = _features(ct.build_tile(*_tile(lon, lat, z))) assert features, "산지 조각에 선이 있어야 함" step, bold = ct.STEP_M[z], ct.BOLD_M[z] levels = [f["properties"]["h"] for f in features] assert len(levels) == len(set(levels)) # 높이 하나 = 지물 하나 for f in features: h, k = f["properties"]["h"], f["properties"]["k"] assert h % step == 0 or h % bold == 0 assert k == int(h % bold == 0) assert f["geometry"]["type"] in ("LineString", "MultiLineString") if z == 15: assert any(h % 10 for h in levels) # 5 m 전부 if z == 14: assert any(h % 25 == 0 and h % 10 for h in levels) # 10 m + 계곡선 25 m def test_rules_dense_to_sparse() -> None: zooms = range(ct.MIN_ZOOM, ct.MAX_ZOOM + 1) steps = [ct.STEP_M[z] for z in zooms] assert steps == sorted(steps, reverse=True) and steps[-1] == 5 assert all(ct.BOLD_M[z] % ct.STEP_M[z] == 0 or z == 14 for z in zooms) def test_empty_tiles() -> None: assert _features(ct.build_tile(*_tile(120.0, 30.0, 10))) == [] # 국내 밖 assert not ct.in_bounds(*_tile(120.0, 30.0, 10)) if ct.NATIONAL_GPKG.is_file(): assert _features(ct.build_tile(*_tile(131.0, 37.8, 13))) == [] # 동해 바다 def test_bad_tile_number() -> None: for z, x, y in [(7, 0, 0), (16, 0, 0), (10, -1, 0), (10, 0, 1024)]: with pytest.raises(KeyError): ct.ensure_tile(z, x, y) @needs_gpkg def test_second_read_is_file(tmp_path, monkeypatch) -> None: monkeypatch.setattr(ct, "TILES_ROOT", tmp_path) lat, lon = sheet5k_center(SHEET) z, x, y = _tile(lon, lat, 15) first = ct.ensure_tile(z, x, y) files = list(tmp_path.rglob("*.pbf")) assert len(files) == 1 and files[0].parts[-3:] == (str(z), str(x), f"{y}.pbf") assert files[0].parent.parent.parent.name.startswith(f"v{ct.RULE_VERSION}_") assert not list(tmp_path.rglob("*.part")) and not list(tmp_path.rglob("*.lock")) def boom(*_args): raise AssertionError("둘째는 파일에서 읽어야 함") monkeypatch.setattr(ct, "build_tile", boom) assert ct.ensure_tile(z, x, y) == first def test_screen_uses_fixed_vector_source() -> None: gl = (ROOT / "ui_template" / "ui_template_gl_map.ts").read_text(encoding="utf-8") assert "export function contourTileSource" in gl and '"vector"' in gl assert "/plan/contour/{z}/{x}/{y}.pbf" in gl base = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_MapGl.ts").read_text(encoding="utf-8") assert "contour: contourTileSource(projectId)" in base specs = (ROOT / "A00_Common" / "map" / "map_layer_specs.ts").read_text(encoding="utf-8") assert specs.count('"source-layer": "contour"') == 3 # 624-2 GL 겹 꼴 = 공용