Files
Aislo/resources/tester/test_581_1_plan_gis_layers.py
T
eomsangdonandClaude Opus 5.5 edc20df849 feat(D 예정노선): 급경사 범위 옵션 모달 — 기울기 % 값 그림 · 1 % 칸 수 · 화면 칠하기 · 범례 ⚙ (PLAN 603 · 요구 466~468)
- 서버: 15 · 35 · 40° 단계 PNG 길을 걷고 기울기 % 값 그림 한 장(칸 = % 내림 0~250 · 빈 칸 255) + 1 % 칸 수 · 파생 저장본 판 2(열쇠 = 격자 지문)
- 화면: 고른 범위대로 값 그림을 칠함(서버 왕복 없음) · 범례 비율 = 칸 수로 바로 셈 · 범례 급경사 줄 ⚙ → 모달(아래 % · 위 % · 색 · ° 환산 · 범위 추가 · 지움 · 기본값 · 빈 값 · 겹침 막음)
- 기본 = 지식DB 15 · 35 · 40° 의 % 26.8 · 70.0 · 83.9 · 기억 = 화면 취향 aislo:pref:d03.slope-bands(계정)
- 시험: 569_1 급경사 시험을 값 그림 · 칸 수로 고침, 581_1 은 bands 대신 counts(옛 단계 응답이 없어짐) · 새 603 시험(칸 수 합 = 유효 칸 · 기본 비율 = 옛 15 · 35 · 40° 비율)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:12:27 +09:00

216 lines
8.6 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 겹 「지금」 묶음(PLAN 581-1) — 계류 · 도로 띠 합침 · 넓이 · 보호구역 잘라 넓이 ·
종류 하나 실패 = 그 종류만 빠짐 · 모두 실패 = 오류 · 덮으면 다시 안 받음 · 음영기복 저장본 따로 ·
도엽 건물 · 물 면 코드.
./venv/Scripts/python.exe -m pytest resources/tester/test_581_1_plan_gis_layers.py -q
"""
import base64
import io
import json
import math
from pathlib import Path
import numpy as np
import pytest
from PIL import Image
from shapely.geometry import LineString
from B04_PreProcess.B04_PreProcess_Engine_SheetParser import SHEET_LAYERS
from common_util.common_util_crs import tm_to_lonlat
from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers
EPSG = 5186
BOX = (230000.0, 450000.0, 232000.0, 451000.0) # 2 × 1 km
def test_sheet_codes() -> None:
assert SHEET_LAYERS["건물"] == ("N3A", "B0010000")
assert SHEET_LAYERS["실폭하천"] == ("N3A", "E0010001")
assert {SHEET_LAYERS["수계면E003"][1], SHEET_LAYERS["수계면E005"][1]} == {
"E0032111",
"E0052114",
}
# ── 띠 ──
def test_band_one_line_area() -> None:
line = LineString([(230500.0, 450500.0), (231500.0, 450500.0)])
band = layers.band_union([line], 100.0, BOX)
assert band.area == pytest.approx(2 * 100 * 1000 + math.pi * 100**2, rel=0.01)
def test_band_overlap_merged_and_clipped() -> None:
a = LineString([(230500.0, 450500.0), (231500.0, 450500.0)])
twin = layers.band_union([a, a], 100.0, BOX)
assert twin.area == pytest.approx(layers.band_union([a], 100.0, BOX).area) # 겹친 곳 한 번
edge = LineString([(230000.0, 450500.0), (232000.0, 450500.0)])
assert layers.band_union([edge], 100.0, BOX).area == pytest.approx(2000 * 200, rel=1e-6)
assert layers.band_union([], 100.0, BOX) is None
far = LineString([(240000.0, 460000.0), (240100.0, 460000.0)])
assert layers.band_union([far], 100.0, BOX) is None
def _line_feature(points: list[tuple[float, float]], **props) -> dict:
coords = [list(tm_to_lonlat(x, y, EPSG)) for x, y in points]
return {
"type": "Feature",
"geometry": {"type": "LineString", "coordinates": coords},
"properties": props,
}
def test_band_layer_filters_and_caches(tmp_path, monkeypatch) -> None:
processed = tmp_path / "B04_PreProcess" / "processed"
processed.mkdir(parents=True)
row = [(230500.0, 450500.0), (231500.0, 450500.0)]
streams = [
_line_feature(row, 구분="지방하천"),
_line_feature([(230200.0, 450100.0), (230200.0, 450900.0)], 구분="세류"),
]
roads = [_line_feature(row, 도로구분="지방도"), _line_feature(row, 도로구분="소로")]
for key, feats in (("하천중심선", streams), ("도로중심선", roads)):
body = {"type": "FeatureCollection", "features": feats}
(processed / f"도엽_{key}.geojson").write_text(
json.dumps(body, ensure_ascii=False), "utf-8"
)
real = layers.band_union
counted = []
monkeypatch.setattr(layers, "band_union", lambda *a: counted.append(1) or real(*a))
value = layers.band_layer(tmp_path, EPSG, BOX)
assert value["stream"]["lines"] == 1 and value["road"]["lines"] == 1 # 세류 · 소로 뺌
assert value["stream"]["area_m2"] == pytest.approx(2e5 + math.pi * 1e4, rel=0.02)
road = 2 * 300 * 1000 + math.pi * 300**2 # 둥근 끝 · 범위 안
assert value["road"]["area_m2"] == pytest.approx(road, rel=0.01)
assert value["road"]["ratio"] == pytest.approx(road / 2e6, abs=0.005)
assert len(value["stream"]["collection"]["features"]) == 1
again = layers.band_layer(tmp_path, EPSG, BOX)
assert again == json.loads(json.dumps(value)) and len(counted) == 2 # 저장본
# ── 보호 · 규제 구역 ──
def _square(x0: float, y0: float, side: float) -> dict:
ring = [(x0, y0), (x0 + side, y0), (x0 + side, y0 + side), (x0, y0 + side), (x0, y0)]
return {"type": "Polygon", "coordinates": [[list(tm_to_lonlat(x, y, EPSG)) for x, y in ring]]}
def test_clip_zones_area_and_names() -> None:
found = {
"forest": [
{
"id": "a",
"geometry": _square(231800.0, 450800.0, 400.0),
"properties": {"alias": "보안림"},
},
{
"id": "a",
"geometry": _square(231800.0, 450800.0, 400.0),
"properties": {},
}, # 조각 겹침
{
"id": "b",
"geometry": _square(240000.0, 460000.0, 100.0),
"properties": {},
}, # 범위 밖
],
"park": [
{
"id": "c",
"geometry": _square(230100.0, 450100.0, 100.0),
"properties": {"park_name": "속리산"},
}
],
}
features, summary = layers.clip_zones(found, BOX, EPSG)
assert summary["forest"]["count"] == 1
assert summary["forest"]["area_m2"] == pytest.approx(200 * 200, rel=0.01) # 귀퉁이만 남음
assert summary["park"]["area_m2"] == pytest.approx(100 * 100, rel=0.01)
assert summary["water"] == {"label": "상수원보호구역", "count": 0, "area_m2": 0.0}
assert [f["properties"]["name"] for f in features] == ["보안림", "속리산"]
assert {f["properties"]["kind"] for f in features} == {"forest", "park"}
class FakeZoneWfs:
"""겹 이름마다 정사각형 하나 · `fail` 에 든 겹 이름은 실패(파일 없음)."""
def __init__(self, fail: frozenset[str] = frozenset()) -> None:
self.fail, self.calls = fail, []
def __call__(self, typename, filename, bounds, output_dir) -> None:
self.calls.append(typename)
if typename in self.fail:
return
feats = [{"id": typename, "geometry": _square(230500.0, 450200.0, 300.0), "properties": {}}]
body = {"type": "FeatureCollection", "features": feats}
(Path(output_dir) / filename).write_text(json.dumps(body), "utf-8")
ALL_NAMES = [name for _, _, names in layers.ZONE_KINDS for name in names]
def test_zones_fetch_once_partial_failure(tmp_path, monkeypatch) -> None:
assert len(layers.ZONE_KINDS) == 8 and len(ALL_NAMES) == 10
fake = FakeZoneWfs(fail=frozenset({"lt_c_uf151"}))
monkeypatch.setattr(layers, "request_vworld_wfs", fake)
got = layers.ensure_zones(tmp_path, BOX, EPSG)
assert (
got["fetched"] and got["failed"] == ["forest"] and sorted(fake.calls) == sorted(ALL_NAMES)
)
saved = json.loads((tmp_path / layers.ZONE_FILE).read_text("utf-8"))
assert saved["summary"]["forest"].get("failed") is True
assert saved["summary"]["park"]["count"] == 3 # 자연공원 겹 셋
assert len(saved["features"]) == 9
ok = FakeZoneWfs()
monkeypatch.setattr(layers, "request_vworld_wfs", ok)
assert layers.ensure_zones(tmp_path, BOX, EPSG)["fetched"] # 실패 종류 있으면 다시
assert not layers.ensure_zones(tmp_path, (230500.0, 450100.0, 231000.0, 450900.0), EPSG)[
"fetched"
]
assert len(ok.calls) == 10 # 덮으면 다시 안 받음
def test_zones_all_fail(tmp_path, monkeypatch) -> None:
monkeypatch.setattr(layers, "request_vworld_wfs", FakeZoneWfs(fail=frozenset(ALL_NAMES)))
with pytest.raises(RuntimeError):
layers.ensure_zones(tmp_path, BOX, EPSG)
assert not (tmp_path / layers.ZONE_FILE).exists()
# ── 음영기복 ──
def test_hillshade_saved_apart_from_slope(tmp_path) -> None:
models = tmp_path / "B04_PreProcess" / "models_plan"
models.mkdir(parents=True)
n = 30
z = np.tile(np.arange(n) * 1.0, (n, 1))
x = 200000.0 + np.arange(n) * 2.0
np.savez(
models / "dtm_sheet_laplace.npz",
x=x,
y=x + 250000.0,
z=z,
valid_mask=np.ones(z.shape, bool),
resolution=[2.0],
)
shade = layers.slope_layer(tmp_path, models, EPSG, "hillshade")
slope = layers.slope_layer(tmp_path, models, EPSG, "slope")
assert shade["image"].startswith("data:image/png;base64,") and shade["bands"] == []
assert slope["counts"] and shade["box_m"] == slope["box_m"] # 603 = 값 그림 + 칸 수
assert layers.slope_layer(tmp_path, models, EPSG, "hillshade") == shade
def test_hillshade_hole_transparent() -> None:
z = np.tile(np.arange(20) * 1.0, (20, 1))
valid = np.ones(z.shape, bool)
valid[:5, :5] = False # 남서 귀퉁이 = 그림 왼쪽 아래
value = layers.hillshade_layer_png(z, valid, 2.0)
alpha = np.asarray(Image.open(io.BytesIO(base64.b64decode(value["image"].split(",", 1)[1]))))[
..., 1
]
assert (alpha[-5:, :5] == 0).all() and (alpha[:10, 10:] == 255).all()