Files
Aislo/resources/tester/test_584_1_plan_geo_lines.py
T
eomsangdonandClaude Opus 5.5 61733d064d feat(지도): 634-2 · 634-3 아이콘 표 두 장 · 묶음 메뉴 · 한 행 배선 · 호버 = 표(요구 546)
- 겹 표(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>
2026-11-01 11:40:26 +09:00

213 lines
8.3 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 노선분석 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)