- 겹 표(map_layer_list) = 그룹1 22 줄 표 차례 · title = 호버제목 · group(수계망도 · 도로 · 토지·건물 · 제한·재해) · pages · 숨김 표 걷음 · 도로300m 한 줄(「도로」 묶음 · 기능 남김) - 겹 줄(map_layer_bar) = 단독 넷 | 묶음 넷(여럿 고르기 메뉴 · 켠 수 배지 · 안 닫힘) | 그래픽 카드 · sync 가 단추 · 메뉴 줄 막힘 · 글을 맞춤(why = 페이지 까닭) - 도구 표(map_toolbar) = 그룹2 17 줄 title · mapToolGroups 가 글을 표로 맞춤 · 기본 단추 생성 · 지형 칩 막힘 「수치지형도/라이다 — 까닭」 - 띠 = 한 자료 · 겹 둘(GL fillEdge 거름 · 옛 캔버스 · 범례 줄 따로) · B05 띠 받기(D03 과 같은 서버 길 · bandCollection 공용) - D03 = 겹 줄 · 도구줄 한 행(createMapToolRow) · 222px 기댐 · 줄바꿈 CSS 걷음 · 측점 · 기울기 호버 덮어쓰기 걷음 · LAS 올리기 호버 걷음 - B05 = 세로 두 줄 → 한 행 · 호버 덮어쓰기 걷음(도구줄 · 1m 등고선 · 3D · 추천) · 겹 막힘 querySelector 덮어쓰기 → 공용 sync · 임도(업로드) 보임 + 「받은 임도 자료 없음」 막힘 · 스냅 간격 날 select → 고르기 메뉴(하나) - 켬끔 기억 열쇠 · 측점 · 기울기 처음 값 · 그래픽 카드 취향 열쇠 둘 그대로 - 낡은 시험 고침(568_1 · 622_2 · 624_2 · 624_3 · 553_4 · 625_draw · b05_routeedit_initial · 626_3 · 390_4 · 346_2 · 353_1 · 581_2 · 584_1 · 584_3) — 표 꼴 row() · 호버 글 = 표 · 한 행 · 고르기 메뉴로 바뀐 글을 잡음 · 새 지킴 634_2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
8.3 KiB
Python
213 lines
8.3 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"),
|
||
):
|
||
# 634-2 표 한 줄 = row(열쇠, 아이콘, 호버제목, 묶음[, true]) · true 없음 = 처음 끔
|
||
entry = re.search(rf'row\("{key}", "(\w+)", "[^"]+", (?:"\w+"|null)(, true)?\)', defs)
|
||
assert entry and not entry.group(2), 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)
|