- 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>
173 lines
6.0 KiB
Python
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)
|