- 서버: 지오빅데이터(KIGAM) 수치지질도 1:50,000 — WFS 암상 면(깨진 한글 칸은 기호별 점 조회로 채움) · 받은 범위 ∪ 노선으로 자름 · 저장본 · 노선 따라 1 m 표본 구간 · 키는 서버만(.env GEOBIGDATA_API_KEY · 쿼리 key) - 그림 조각 대리에 geology(키 쿼리 이름 칸 key_param · 옛 겹 serviceKey 그대로) - 화면(D03 · B05): 「제한·재해」에 「암상 지질도」 — 그림 조각 + 갈래 색 면 · 「기호 · 암상」 글 · 범례 · 노선 구간 표(참고 — 토사 · 암 판정 아님) - 켠 밖 겹마다 범례 맨 아래 출처 줄(브이월드 · 국토지리정보원 · 산림청 · 환경부 · 생활안전지도 공공누리 · 한국지질자원연구원 CC BY-NC) - 범례 칸 높이 한도 · 밀기(낮은 화면) - 시험: test_760_1 새로 · test_634_2 · test_553_4 겹 차례 표에 geology 한 줄 더함(새 겹 · 낡은 표) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
173 lines
7.4 KiB
Python
173 lines
7.4 KiB
Python
"""760-1 암상 지질도 겹 — 바위 갈래 · WFS 깨진 칸 읽기 · 받은 범위 자름 · 노선 구간 · 키 숨김 · 출처 줄."""
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from common_util import common_util_wms_tiles as wms_tiles
|
|
from D03_PlanAnalysis import D03_PlanAnalysis_Geology as geology
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
MAP = ROOT / "A00_Common" / "map"
|
|
EPSG = 5179
|
|
FAKE_KEY = "TESTKEY-760-SECRET"
|
|
|
|
|
|
def _square(x0, y0, size):
|
|
from pyproj import Transformer
|
|
|
|
to_ll = Transformer.from_crs(f"EPSG:{EPSG}", "EPSG:4326", always_xy=True).transform
|
|
ring = [(x0, y0), (x0 + size, y0), (x0 + size, y0 + size), (x0, y0 + size), (x0, y0)]
|
|
return {"type": "Polygon", "coordinates": [[list(to_ll(x, y)) for x, y in ring]]}
|
|
|
|
|
|
def test_rock_group():
|
|
assert geology.rock_group("알코스사암 역질 사암 셰일 역암") == 2
|
|
assert geology.rock_group("흑운모 화강암") == 0
|
|
assert geology.rock_group("우백질 화강편마암") == geology.META
|
|
assert geology.rock_group("결정질 석회암 및 각섬암") == geology.META
|
|
assert geology.rock_group("안산암질 응회암") == 1
|
|
assert geology.rock_group("충적층") == 4
|
|
assert geology.rock_group("") == geology.OTHER
|
|
assert len(geology.ROCK_GROUP_NAMES) == 6
|
|
|
|
|
|
def test_parse_litho_broken_korean_keys():
|
|
# 서버 흠 — 한글 칸 이름 · 값이 「??」로 겹침 · 기호만 영문
|
|
body = (
|
|
'{"type":"FeatureCollection","features":[{"type":"Feature","geometry":'
|
|
'{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,0]]]},'
|
|
'"properties":{"??":"???? ???","??":"PCEp","????":"?? ??","symnum":"010106",'
|
|
'"mapname":"???","mapidx":"IF04"}}]}'
|
|
).encode()
|
|
got = geology.parse_litho(body)
|
|
assert got == [
|
|
{
|
|
"symnum": "010106",
|
|
"symbol": "PCEp",
|
|
"geometry": {"type": "Polygon", "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 0]]]},
|
|
}
|
|
]
|
|
with pytest.raises(RuntimeError):
|
|
geology.parse_litho(b"<html>500</html>")
|
|
|
|
|
|
def _layer():
|
|
raw = [
|
|
{"symnum": "A1", "symbol": "Kd", "geometry": _square(1000000, 2000000, 100)},
|
|
{"symnum": "B2", "symbol": "Kg", "geometry": _square(1000100, 2000000, 100)},
|
|
{"symnum": "A1", "symbol": "Kd", "geometry": _square(1000900, 2000900, 50)}, # 범위 밖
|
|
]
|
|
names = {"A1": "알코스사암 셰일", "B2": "흑운모 화강암"}
|
|
calls = []
|
|
|
|
def fake_names(symnum, symbol, lon, lat):
|
|
calls.append(symnum)
|
|
return {
|
|
"symbol": symbol,
|
|
"litho": names[symnum],
|
|
"strata": "지층",
|
|
"age": "백악기",
|
|
"sheet": "도계동",
|
|
}
|
|
|
|
box = (1000000.0, 2000000.0, 1000200.0, 2000100.0)
|
|
return geology.build_layer(raw, box, EPSG, fake_names), calls
|
|
|
|
|
|
def test_build_layer_clips_and_names_once_per_symbol():
|
|
value, calls = _layer()
|
|
assert sorted(calls) == ["A1", "B2"] # 기호마다 점 조회 한 번
|
|
assert [(s["symbol"], s["group"], s["count"]) for s in value["summary"]] == [
|
|
("Kd", 2, 1),
|
|
("Kg", 0, 1),
|
|
]
|
|
assert value["summary"][0]["area_m2"] == pytest.approx(10000, rel=1e-3)
|
|
assert len(value["features"]) == 2 and len(value["parts_m"]) == 2
|
|
lon, lat = value["features"][0]["geometry"]["coordinates"][0][0]
|
|
assert 120 < lon < 135 and 30 < lat < 45 # 화면 지물 = 경위도
|
|
|
|
|
|
def test_route_segments():
|
|
value, _ = _layer()
|
|
# 서 → 동 200 m 직선 — 0~100 m Kd · 100~200 m Kg
|
|
segs = geology.route_segments(value["parts_m"], [[1000000, 2000050], [1000200, 2000050]])
|
|
assert [(s["from_m"], s["to_m"], s["i"]) for s in segs] == [(0.0, 100.0, 0), (100.0, 200.0, 1)]
|
|
# 범위 밖으로 50 m 더 = 자료 없음(-1)
|
|
segs = geology.route_segments(value["parts_m"], [[1000000, 2000050], [1000250.5, 2000050]])
|
|
assert segs[-1] == {"from_m": 200.0, "to_m": 250.5, "i": -1}
|
|
assert geology.route_segments(value["parts_m"], [[0, 0]]) == []
|
|
out = geology.layer_response(
|
|
value, [{"name": "계획노선", "points": [[1000000, 2000050], [1000200, 2000050]]}]
|
|
)
|
|
assert "parts_m" not in out and out["routes"][0]["segments"][1]["i"] == 1
|
|
assert "CC BY-NC" in out["source"]
|
|
|
|
|
|
def test_cover_box_takes_route():
|
|
box = (0.0, 0.0, 100.0, 100.0)
|
|
assert geology.cover_box(box, []) == box
|
|
got = geology.cover_box(box, [{"name": "a", "points": [[50, 50], [300, 40]]}], pad=10)
|
|
assert got == (0.0, 0.0, 310.0, 100.0)
|
|
|
|
|
|
def test_key_only_on_server(monkeypatch):
|
|
"""키 = 쿼리 `key` 로만 · 오류 글에 주소 · 키 없음 · 없으면 MissingKeyError."""
|
|
seen = []
|
|
|
|
def fake_urlopen(request, timeout):
|
|
seen.append(request.full_url)
|
|
raise OSError("boom")
|
|
|
|
monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
|
|
monkeypatch.setenv(geology.KEY_ENV, FAKE_KEY)
|
|
with pytest.raises(RuntimeError) as error:
|
|
geology.fetch_litho({"min_lon": 129, "min_lat": 36, "max_lon": 129.1, "max_lat": 36.1})
|
|
assert FAKE_KEY not in str(error.value)
|
|
assert f"key={FAKE_KEY}" in seen[0] and "typeName=geoOpen" in seen[0]
|
|
# 그림 조각 대리 — 같은 키 · 쿼리 이름 key · 오류 글에 키 없음
|
|
source = wms_tiles.SOURCES["geology"]
|
|
assert source.key_param == "key" and source.key_env == geology.KEY_ENV
|
|
with pytest.raises(RuntimeError) as error:
|
|
wms_tiles.fetch("geology", 14, 14000, 6400)
|
|
assert FAKE_KEY not in str(error.value)
|
|
assert seen[1].startswith(f"{geology.WMS_URL}?key={FAKE_KEY}&") and "GetMap" in seen[1]
|
|
assert "serviceKey" in wms_tiles.SOURCES["landslide"].key_param # 옛 겹 그대로
|
|
monkeypatch.setenv(geology.KEY_ENV, "")
|
|
with pytest.raises(geology.MissingKeyError):
|
|
geology.fetch_litho({"min_lon": 129, "min_lat": 36, "max_lon": 129.1, "max_lat": 36.1})
|
|
|
|
|
|
def test_screen_layer_and_sources():
|
|
lst = (MAP / "map_layer_list.ts").read_text(encoding="utf-8")
|
|
assert 'row("geology", "strata", "암상 지질도", "limit")' in lst
|
|
src = (MAP / "map_layer_source.ts").read_text(encoding="utf-8")
|
|
assert re.search(r'\["브이월드", \[[^\]]*"faults", "lineaments"\]\]', src)
|
|
assert re.search(r'CC BY-NC\)", \["geology"\]\]', src)
|
|
assert '공공누리 제4유형)", ["landslide"]' in src
|
|
forest = (MAP / "map_forest_layers.ts").read_text(encoding="utf-8")
|
|
assert 'geology_map: wmt("geology")' in forest
|
|
geo = (MAP / "map_geology.ts").read_text(encoding="utf-8")
|
|
assert "참고 — 토사 · 암 판정 아님" in geo and "/plan/layers/geology" in geo
|
|
assert "data.kigam.re.kr" not in geo # 브라우저는 KIGAM 을 부르지 않음
|
|
theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text(encoding="utf-8")
|
|
assert all(f"--draw-map-geology-{i}:" in theme for i in range(1, 7))
|
|
|
|
|
|
def test_no_key_in_code():
|
|
env = (ROOT / ".env").read_text(encoding="utf-8") if (ROOT / ".env").is_file() else ""
|
|
got = re.search(r"^GEOBIGDATA_API_KEY=(.+)$", env, re.M)
|
|
if not got or not got.group(1).strip():
|
|
pytest.skip("키 없는 PC")
|
|
key = got.group(1).strip()
|
|
paths = [
|
|
ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_Geology.py",
|
|
MAP / "map_geology.ts",
|
|
MAP / "map_layer_source.ts",
|
|
]
|
|
leaked = [p.name for p in paths if key in p.read_text(encoding="utf-8")] # 실패 글에 키 안 냄
|
|
assert leaked == []
|
|
assert json.dumps(geology.SOURCE) # 출처 글은 있음
|