Files
Aislo/resources/tester/test_603_plan_slope_bands.py
T
eomsangdonandClaude Opus 5.5 c6a1efad0c refactor(공용 지도): 겹 받기 · 범례 줄 · 급경사 범위 · 위성 밝기 · 겹 자료 도움을 A00_Common/map 으로 — D03 은 다시 내놓기 · 동작 그대로 (PLAN 624-3 · 요구 530)
- D03_Api 겹 받기 → map_layer_api.ts(D03_Api 가 다시 내놓음)
- UI_SlopeBands · logic SlopeBands · UI_SatLight → map_slope_bands · logic/map_slope_bands · map_sat_light
- 범례 줄 legendRows → map_legend.ts(받은 자료 꼴 LegendData · D03 LayerLegend 가 다시 내놓음)
- glImage · glTagged · shadeImage · loadImage · WATER_KEYS · FOREST_ROAD_KINDS · hectare → map_layer_data.ts
- 시험 경로 고침 — test_603(TS 자리) · test_606_1(밝기 파일) · test_584_3(범례 글 = 공용 파일도 읽음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:47:50 +09:00

173 lines
6.0 KiB
Python

"""D03 급경사 범위(PLAN 603 · 요구 466 · 467 · 468) — 서버 1 % 칸 수 +
`A00_Common/map/logic/map_slope_bands.ts`(624-3 공용으로 옮김).
1 % 칸 수 합 = 유효 칸 · 기본 범위(26.8 · 70.0 · 83.9 %) 비율 = 옛 15 · 35 · 40° 비율 ·
범위 막음(빈 값 · 0~250 밖 · 아래 ≥ 위 · 겹침 · 없음) · 깨진 취향 = 기본 · 250 % 이상 칸 = 끝 범위.
TS 를 tsc 로 옮겨 node 로 돌림(서버 · 화면 없이).
./venv/Scripts/python.exe -m pytest resources/tester/test_603_plan_slope_bands.py -q
"""
from __future__ import annotations
import json
import re
import subprocess
from pathlib import Path
import numpy as np
import pytest
from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers
from E07_StabArea.E07_StabArea_Engine import slope_rad
ROOT = Path(__file__).resolve().parents[2]
TS_FILE = ROOT / "A00_Common" / "map" / "logic" / "map_slope_bands.ts"
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
CELL = 2.0
OLD_DEG = ((15.0, 35.0), (35.0, 40.0), (40.0, 90.0)) # 옛 SLOPE_BANDS
def _hills(n: int = 300) -> np.ndarray:
"""0 ~ 60° 남짓 고루 섞인 언덕(구멍 하나)."""
y, x = np.mgrid[0:n, 0:n] * CELL
return 40 * np.sin(x / 37) * np.cos(y / 53) + 25 * np.sin((x + 2 * y) / 19) + 0.3 * x
_RUNNER = """
import { readFileSync, writeFileSync } from "node:fs";
import * as bands from "./A00_Common/map/logic/map_slope_bands.js";
const cases = JSON.parse(readFileSync(process.argv[2], "utf8"));
const out = { DEFAULT: bands.DEFAULT_SLOPE_BANDS };
// 인자 null = 기본 범위
for (const [key, [name, ...args]] of Object.entries(cases))
out[key] = bands[name](...args.map((a) => (a === null ? bands.DEFAULT_SLOPE_BANDS : a)));
writeFileSync(process.argv[3], JSON.stringify(out));
"""
C = "--draw-map-landslide-3"
@pytest.fixture(scope="module")
def hills() -> dict:
z = _hills()
valid = np.ones(z.shape, bool)
valid[100:140, 100:180] = False
value = layers.slope_values_png(z, valid, CELL)
deg = np.degrees(slope_rad(np.where(valid, z, np.nan), CELL))
known = np.isfinite(deg)
old = [float(((deg >= lo) & (deg < hi) & known).sum() / known.sum()) for lo, hi in OLD_DEG]
return {"counts": value["counts"], "known": int(known.sum()), "old": old}
@pytest.fixture(scope="module")
def r(tmp_path_factory: pytest.TempPathFactory, hills: dict) -> dict:
tmp = tmp_path_factory.mktemp("slope_bands")
out = tmp / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(TS_FILE),
"--outDir",
str(out),
"--rootDir",
str(ROOT),
"--module",
"esnext",
"--target",
"es2022",
"--moduleResolution",
"bundler",
"--ignoreConfig",
],
cwd=str(ROOT),
check=True,
capture_output=True,
)
for emitted in out.rglob("*.js"):
emitted.write_text(
re.sub(r'(from\s+"\.\.?/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
encoding="utf-8",
)
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
band = lambda lo, hi: {"low": lo, "high": hi, "color": C} # noqa: E731
top = [0] * 250 + [7]
cases = {
"default_ratios": ["bandRatios", hills["counts"], None],
"user_ratios": [
"bandRatios",
hills["counts"],
[band(10, 15), band(15, 20), band(20, 250), band(0, 10)],
],
"top": ["bandRatios", top, [band(83.9, 250)]],
"top_closed": ["bandRatios", top, [band(83.9, 249)]],
"ok": ["checkBands", [band(10, 15), band(15, 20), band(20, 250), band(0, 10)]],
"overlap": ["checkBands", [band(10, 20), band(15, 30)]],
"empty_value": ["checkBands", [band(None, 20)]],
"out_of_range": ["checkBands", [band(10, 260)]],
"upside_down": ["checkBands", [band(30, 20)]],
"none": ["checkBands", []],
"bad_color": ["checkBands", [{"low": 1, "high": 2, "color": "red"}]],
"parse_broken": ["parseBands", "{oops"],
"parse_overlap": ["parseBands", json.dumps([band(10, 20), band(15, 30)])],
"parse_ok": ["parseBands", json.dumps([band(10, 20)])],
"deg": ["pctToDeg", 100],
}
job, result = tmp / "job.json", tmp / "out.json"
job.write_text(json.dumps(cases), encoding="utf-8")
subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(job), str(result)],
cwd=str(ROOT),
check=True,
capture_output=True,
)
return json.loads(result.read_text(encoding="utf-8"))
def test_칸_수_합은_유효_칸(hills):
assert sum(hills["counts"]) == hills["known"]
assert len(hills["counts"]) == layers.SLOPE_MAX_PCT + 1
def test_기본은_지식DB_15_35_40도(r):
assert [(b["low"], b["high"]) for b in r["DEFAULT"]] == [
(26.8, 70.0),
(70.0, 83.9),
(83.9, 250),
]
def test_기본_비율은_옛_15_35_40도_비율과_같음(r, hills):
"""1 % 칸 경계(26.8 → 27 · 83.9 → 84 %) 차이만 — 범례 0.1 % 자리 반 칸 안."""
for new, old in zip(r["default_ratios"], hills["old"], strict=True):
assert old > 0.02
assert new == pytest.approx(old, abs=0.002)
def test_사용자_범위_넷(r):
ratios = r["user_ratios"]
assert len(ratios) == 4 and sum(ratios) == pytest.approx(1.0)
def test_250_퍼센트_이상은_끝_범위(r):
assert r["top"] == [1.0]
assert r["top_closed"] == [0.0]
def test_범위_막음(r):
assert r["ok"] is None
assert "겹칩니다" in r["overlap"]
assert "비었습니다" in r["empty_value"]
assert "0~250" in r["out_of_range"]
assert "아래가 위보다" in r["upside_down"]
assert "하나는" in r["none"]
assert "색" in r["bad_color"]
def test_깨진_취향은_기본(r):
assert r["parse_broken"] == r["DEFAULT"]
assert r["parse_overlap"] == r["DEFAULT"]
assert r["parse_ok"] == [{"low": 10, "high": 20, "color": C}]
assert r["deg"] == pytest.approx(45.0)