- map_layer_list = 겹 스물셋 · 처음 값 · 페이지 표(d03 · b05 보일 겹 · 처음 켬 · 취향 열쇠 — A② · A⑨) - map_layer_specs = GL 겹 꼴 · 차례(D03 UI_MapGl 에서 옮김 · 선 겹 꼴은 LINE_LAYERS 한 벌) - map_layer_bar = 겹 단추 줄 · 켬끔 · 취향 · 그래픽 카드 단추 / map_legend = 범례 칸 · 조각 겹 범례 - map_forest_layers = D03_UI_ForestLayers 옮김(이름만) - D03 UI_Layers 683 → 639 줄 · UI_MapGl 423 → 115 줄 · UI_LayerDefs 는 캔버스 도움 + 옛 이름 다시 내놓음 - 시험 길 고침(553-4 · 581-2 · 584-1 · 584-3 · 606-1 · 615 · 622-2) — 옮긴 파일 자리 · 553-4 겹 목록은 622-2 때 낡은 것(국유림 임도 · 경제림 · 생태자연도 빠짐) 같이 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
212 lines
8.2 KiB
Python
212 lines
8.2 KiB
Python
"""D 노선분석 GIS 겹 더함 2(PLAN 584-1) — 단층 · 지질구조선 잘라 가닥 · 길이 · 실패 = 오류 ·
|
||
덮으면 다시 안 받음 · 산사태위험지도(키 없음 · 그림 아님 = 오류 · 옮겨 그림 · 덮으면 저장본) ·
|
||
0차곡 흐름누적(웅덩이 · 평지 넘김) · 임계 · 선 뽑기 · 저장본 · 화면 겹 정의.
|
||
|
||
./venv/Scripts/python.exe -m pytest resources/tester/test_584_1_plan_geo_lines.py -q
|
||
"""
|
||
|
||
import base64
|
||
import io
|
||
import json
|
||
import math
|
||
import re
|
||
from pathlib import Path
|
||
|
||
import numpy as np
|
||
import pytest
|
||
|
||
from common_util.common_util_crs import tm_to_lonlat
|
||
from D03_PlanAnalysis import D03_PlanAnalysis_Landslide as landslide
|
||
from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers
|
||
from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zo
|
||
|
||
EPSG = 5186
|
||
BOX = (230000.0, 450000.0, 232000.0, 451000.0) # 2 × 1 km
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
|
||
|
||
def _line(gid: str, points: list[tuple[float, float]]) -> dict:
|
||
coords = [list(tm_to_lonlat(x, y, EPSG)) for x, y in points]
|
||
return {"id": gid, "geometry": {"type": "LineString", "coordinates": coords}, "properties": {}}
|
||
|
||
|
||
# ── 선 겹 ──
|
||
|
||
|
||
def test_clip_lines_length() -> None:
|
||
found = [
|
||
_line("a", [(229000.0, 450500.0), (231000.0, 450500.0)]), # 반만 안 = 1,000 m
|
||
_line("a", [(229000.0, 450500.0), (231000.0, 450500.0)]), # 겹침
|
||
_line("b", [(231500.0, 450100.0), (231500.0, 450400.0)]), # 300 m
|
||
_line("c", [(240000.0, 460000.0), (240100.0, 460000.0)]), # 밖
|
||
]
|
||
features, summary = layers.clip_lines(found, BOX, EPSG)
|
||
assert summary["count"] == 2 == len(features)
|
||
assert summary["length_m"] == pytest.approx(1300.0, rel=0.005)
|
||
assert features[0]["properties"]["length_m"] == pytest.approx(1000.0, rel=0.005)
|
||
|
||
|
||
class FakeWfs:
|
||
def __init__(self, fail: bool = False) -> None:
|
||
self.fail, self.calls = fail, []
|
||
|
||
def __call__(self, typename, filename, bounds, output_dir) -> None:
|
||
self.calls.append(typename)
|
||
if self.fail:
|
||
return
|
||
feats = [_line("a", [(230100.0, 450100.0), (230100.0, 450600.0)])]
|
||
body = {"type": "FeatureCollection", "features": feats}
|
||
(Path(output_dir) / filename).write_text(json.dumps(body), "utf-8")
|
||
|
||
|
||
@pytest.mark.parametrize("kind", list(layers.LINE_KINDS))
|
||
def test_lines_fetch_once(tmp_path, monkeypatch, kind) -> None:
|
||
fake = FakeWfs()
|
||
monkeypatch.setattr(layers, "request_vworld_wfs", fake)
|
||
label, typename = layers.LINE_KINDS[kind]
|
||
assert layers.ensure_lines(tmp_path, BOX, EPSG, kind)["fetched"] and fake.calls == [typename]
|
||
saved = json.loads((tmp_path / f"{label}_bounds.geojson").read_text("utf-8"))
|
||
assert saved["summary"]["count"] == 1
|
||
assert saved["summary"]["length_m"] == pytest.approx(500.0, rel=0.005)
|
||
inner = (230050.0, 450050.0, 231000.0, 450900.0)
|
||
assert not layers.ensure_lines(tmp_path, inner, EPSG, kind)["fetched"] # 덮으면 다시 안 받음
|
||
assert layers.ensure_lines(tmp_path, (229000.0, *BOX[1:]), EPSG, kind)["fetched"]
|
||
|
||
|
||
def test_lines_fail_raises(tmp_path, monkeypatch) -> None:
|
||
monkeypatch.setattr(layers, "request_vworld_wfs", FakeWfs(fail=True))
|
||
with pytest.raises(RuntimeError):
|
||
layers.ensure_lines(tmp_path, BOX, EPSG, "faults")
|
||
assert not list(tmp_path.iterdir())
|
||
|
||
|
||
def test_line_kinds_vworld_names() -> None:
|
||
assert layers.LINE_KINDS["faults"][1] == "lt_l_gimsfault"
|
||
assert layers.LINE_KINDS["lineaments"][1] == "lt_l_gimslinea"
|
||
|
||
|
||
# ── 산사태위험지도 ──
|
||
|
||
|
||
def _png(width: int, height: int, rgba: tuple[int, int, int, int]) -> bytes:
|
||
from PIL import Image
|
||
|
||
buffer = io.BytesIO()
|
||
Image.new("RGBA", (width, height), rgba).save(buffer, format="PNG")
|
||
return buffer.getvalue()
|
||
|
||
|
||
def test_landslide_no_key(tmp_path, monkeypatch) -> None:
|
||
monkeypatch.setenv(landslide.LANDSLIDE_KEY_ENV, " ")
|
||
with pytest.raises(landslide.MissingKeyError, match="키 없음"):
|
||
landslide.ensure_landslide(tmp_path, BOX, EPSG)
|
||
assert not list(tmp_path.iterdir())
|
||
|
||
|
||
def test_landslide_not_image(tmp_path, monkeypatch) -> None:
|
||
class Reply(io.BytesIO):
|
||
def __enter__(self):
|
||
return self
|
||
|
||
def __exit__(self, *exc) -> None:
|
||
pass
|
||
|
||
monkeypatch.setenv(landslide.LANDSLIDE_KEY_ENV, "k")
|
||
monkeypatch.setattr(landslide.urllib.request, "urlopen", lambda *a, **k: Reply(b"<xml/>"))
|
||
with pytest.raises(RuntimeError, match="그림이 아닌"):
|
||
landslide.ensure_landslide(tmp_path, BOX, EPSG)
|
||
assert not list(tmp_path.iterdir())
|
||
|
||
|
||
def test_landslide_warp_and_saved(tmp_path, monkeypatch) -> None:
|
||
from PIL import Image
|
||
|
||
calls = []
|
||
|
||
def fake(bounds, width, height):
|
||
calls.append((width, height))
|
||
return _png(width, height, (220, 38, 38, 200))
|
||
|
||
monkeypatch.setattr(landslide, "request_wms", fake)
|
||
value = landslide.ensure_landslide(tmp_path, BOX, EPSG)
|
||
assert calls == [(1000, 500)] # 2 m/px
|
||
assert value["box_m"] == list(BOX)
|
||
raw = base64.b64decode(value["image"].split(",", 1)[1])
|
||
image = np.asarray(Image.open(io.BytesIO(raw)))
|
||
assert image.shape == (500, 1000, 4)
|
||
assert tuple(image[250, 500]) == (220, 38, 38, 200) # 가운데 = 받은 색
|
||
inner = (230100.0, 450100.0, 231000.0, 450900.0)
|
||
assert landslide.ensure_landslide(tmp_path, inner, EPSG) == value and len(calls) == 1
|
||
|
||
|
||
# ── 0차곡 ──
|
||
|
||
|
||
def _valley(n: int = 60) -> np.ndarray:
|
||
"""가운데(열 n/2)로 모이고 북(행 끝)에서 남으로 내려가는 V 골짜기 — 행 = 남 → 북."""
|
||
r, c = np.mgrid[0:n, 0:n].astype(float)
|
||
return 0.5 * r + 0.8 * np.abs(c - n // 2)
|
||
|
||
|
||
def test_accumulation_valley_floor() -> None:
|
||
z = _valley()
|
||
acc = zo.d8_accumulation(z, np.ones(z.shape, bool))
|
||
assert acc[0, 30] == acc.max() # 골 바닥 남쪽 끝이 가장 큼
|
||
assert acc[0, 30] > 0.4 * z.size # 반쯤 넘게 모임
|
||
|
||
|
||
def test_accumulation_crosses_pit_and_flat() -> None:
|
||
z = _valley()
|
||
z[30, 30] -= 5.0 # 웅덩이
|
||
z[20:24, 20:41] = z[22, 30] # 평지 띠(등고선 칸)
|
||
acc = zo.d8_accumulation(z, np.ones(z.shape, bool))
|
||
assert acc[0, 30] > 0.4 * z.size # 웅덩이 · 평지에서 안 끊김
|
||
|
||
|
||
def test_zero_order_mask_threshold() -> None:
|
||
acc = np.array([[10.0, 249.0, 250.0, 2499.0, 2500.0]])
|
||
mask = zo.zero_order_mask(acc, np.ones(acc.shape, bool), 2.0, 1000.0, 10000.0) # 칸 4 m²
|
||
assert mask.tolist() == [[False, False, True, True, False]]
|
||
|
||
|
||
def test_zero_order_lines_head_only() -> None:
|
||
n = 60
|
||
z = _valley(n)
|
||
x = y = np.arange(n) * 2.0
|
||
lines = zo.zero_order_lines(z, np.ones(z.shape, bool), x, y, 2.0, min_m2=400.0, max_m2=2000.0)
|
||
assert lines
|
||
# 골 바닥(x = 60 m) 위 · 집수 400 ~ 2,000 m² 사이만(골 위쪽 머리)
|
||
floor = sorted(p[1] for line in lines for p in line if abs(p[0] - 60.0) < 1e-6)
|
||
assert floor and floor[0] > 0.0 and floor[-1] < y[-1] # 남 끝(1차 계류) · 북 끝 빠짐
|
||
for line in lines:
|
||
assert sum(math.dist(a, b) for a, b in zip(line, line[1:])) >= zo.ZERO_ORDER_MIN_LEN_M
|
||
|
||
|
||
def test_zero_order_no_dem(tmp_path) -> None:
|
||
with pytest.raises(FileNotFoundError):
|
||
zo.zero_order_layer(tmp_path, tmp_path, EPSG)
|
||
|
||
|
||
# ── 화면 ──
|
||
|
||
|
||
def test_screen_defs_icons_colors() -> None:
|
||
defs = (ROOT / "A00_Common" / "map" / "map_layer_list.ts").read_text("utf-8") # 624-2
|
||
icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8")
|
||
theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
|
||
for key, color in (
|
||
("faults", "fault"),
|
||
("lineaments", "lineament"),
|
||
("zeroOrder", "zero-order"),
|
||
):
|
||
entry = re.search(rf'key: "{key}",\s*icon: "(\w+)",[^}}]*on: (\w+)', defs)
|
||
assert entry and entry.group(2) == "false", key # 처음 값 끔
|
||
assert re.search(rf"^\s+{entry.group(1)}:", icons, re.M), key
|
||
assert f"--draw-map-{color}:" in theme and f'"--draw-map-{color}"' in defs
|
||
for kind in layers.LINE_KINDS:
|
||
assert f'kind: "{kind}"' in defs # 서버 열쇠와 같음
|
||
page = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_LayerPaint.ts").read_text(
|
||
"utf-8"
|
||
) # 586-2
|
||
assert page.count("base: true,") == page.count("draw: (context, map) =>") # 겹 모두 바탕(582)
|