- 연결 점 = 거래처 · 기준점 상자 왼쪽 위 안쪽 · 기준점 = 시점 · 다른 기준점에 놓음(돌기면 취소 + 알림) - 기준점 상자 주소 · 주소 줄바꿈(최대 3줄) + 상자 높이 = 내용 · 항목 글 13px - 선 글 = 구간마다 두 줄(도로 구분 / km) · 처음 자리 선 길이 = 100 + 90·log10(1 + km)(100 ~ 320) - 밀어 냄 = 놓인 상자 · 놓인 선 둘 다 피함 - 시험 = test_254_2 새로 · 244 · 250 기대값(두 줄 · 짧아진 선) · 244_9 node 입력 = stdin Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
130 lines
5.8 KiB
Python
130 lines
5.8 KiB
Python
"""254-2 B08 거리이정표 다이어그램(사용자 162-4 · 5 · 6 · 10) — 기준점 상자 연결 점 · 주소 ·
|
|
연결 점 왼쪽 위 안쪽 · 항목 글 = 거래처 글 · 주소 줄바꿈 + 상자 높이 · 선 글 두 줄 ·
|
|
선 길이 = 거리 비례(로그 · 최소 · 최대).
|
|
"""
|
|
|
|
import json
|
|
import math
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
|
B08 = "B08_Quantity/B08_Quantity_"
|
|
LONG = "경상남도 거창군 북상면 병곡리 968-31"
|
|
|
|
|
|
def _read(rel):
|
|
return (ROOT / rel).read_text(encoding="utf-8")
|
|
|
|
|
|
def _node(expr):
|
|
src = IMPORTS.sub("", _read(B08 + "Milestone_Layout.ts"))
|
|
result = subprocess.run(
|
|
["node", "--experimental-strip-types", "--no-warnings",
|
|
"--input-type=module-typescript", "-"],
|
|
input=src + f"\nconsole.log(JSON.stringify({expr}));",
|
|
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
|
) # fmt: skip
|
|
assert result.returncode == 0, result.stderr
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
def _leg(km):
|
|
return {"road": None, "km": km}
|
|
|
|
|
|
def _route(rid, dir, km, to="site", address=""):
|
|
return {"id": rid, "name": rid, "address": address, "dir": dir, "to": to,
|
|
"legs": [_leg(km)] if km is not None else [],
|
|
"rows": [{"item": "장비", "dest": "start", "note": ""}]} # fmt: skip
|
|
|
|
|
|
def _base(bid, to, km, address="", lon=None, lat=None):
|
|
return {"id": bid, "name": bid, "address": address, "dir": None, "to": to, "lon": lon,
|
|
"lat": lat, "legs": [_leg(km)], "linked": True} # fmt: skip
|
|
|
|
|
|
def _data(bases, routes):
|
|
return {"site": {"mid_station_m": None, "mid_road": "비포장"}, "bases": bases, "routes": routes}
|
|
|
|
|
|
def _lay(data):
|
|
return _node(f"layoutMilestone({json.dumps(data, ensure_ascii=False)}, null, 0.5)")
|
|
|
|
|
|
def _boxes(lay):
|
|
return {b["id"]: b for b in lay["boxes"]}
|
|
|
|
|
|
def test_선_길이_거리_비례_최소_최대():
|
|
kms = [0, 1, 10, 23.8, 100, 1e6, -3]
|
|
got = _node(f"{json.dumps(kms)}.map(lineLen)")
|
|
want = [min(320, max(100, 100 + 90 * math.log10(1 + max(0, k)))) for k in kms]
|
|
assert all(abs(a - b) < 1e-9 for a, b in zip(got, want))
|
|
assert got[0] == 100 and got[5] == 320 and got[6] == 100 # 최소 · 최대(너무 멀지 않게)
|
|
assert got[0] < got[1] < got[2] < got[3] < got[4]
|
|
# 동쪽 거래처 셋 — 공사지 테두리 → 상자 테두리 = 그 구간 선 길이
|
|
lay = _lay(_data([], [_route("a", "E", 1), _route("b", "S", 60), _route("c", "W", None)]))
|
|
boxes = _boxes(lay)
|
|
site = boxes["site"]
|
|
a, b, c = boxes["a"], boxes["b"], boxes["c"]
|
|
assert abs((a["x"]) - (site["x"] + site["w"]) - got[1]) < 1e-6
|
|
assert abs(b["y"] - (site["y"] + site["h"]) - _node("lineLen(60)")) < 1e-6
|
|
assert abs(site["x"] - (c["x"] + c["w"]) - 100) < 1e-6 # 빈 km = 최소
|
|
|
|
|
|
def test_기준점_사슬_자리_주소_높이():
|
|
bases = [_base("A", "B", 4.5, address=LONG), _base("B", "site", 0.6, address="거창읍")]
|
|
lay = _lay(_data(bases, [_route("g", "N", 23.8, to="A")]))
|
|
boxes = _boxes(lay)
|
|
a, b, g = boxes["A"], boxes["B"], boxes["g"]
|
|
assert (a["kind"], b["kind"]) == ("base", "base")
|
|
# 기준점 상자 = 주소(줄바꿈) · 높이 = 이름 + 주소 줄
|
|
assert a["notes"] == ["경상남도 거창군 북상면", "병곡리 968-31"] and a["tip"] == LONG
|
|
assert a["h"] == 3 * 15 + 13 and b["notes"] == ["거창읍"] and b["h"] == 58
|
|
# B = 공사지 서쪽(좌표 없음) · A = B 둘레(B 에 이음) · g = A 북쪽
|
|
site = boxes["site"]
|
|
assert b["x"] + b["w"] < site["x"] and a["x"] + a["w"] < b["x"] + b["w"]
|
|
assert g["y"] + g["h"] < a["y"]
|
|
lines = {(ln["from"], ln["to"]) for ln in lay["lines"]}
|
|
assert {("g", "A"), ("A", "B"), ("B", "start"), ("start", "mid")} == lines
|
|
# 아주 긴 주소 = 최대 3줄 · 끝 줄 줄임표
|
|
many = " ".join([LONG] * 4)
|
|
rows = _node(f'wrap("{many}", 132, 10)')
|
|
assert len(rows) == 3 and rows[-1].endswith("…")
|
|
assert all(_node(f'textW("{r}", 10)') <= 132 for r in rows)
|
|
|
|
|
|
def test_선_글_두_줄():
|
|
lay = _lay(_data([_base("B", "site", 0.6)], [_route("g", "N", 23.8, to="B")]))
|
|
line = next(ln for ln in lay["lines"] if ln["from"] == "B")
|
|
assert line["labels"] == ["1차포장\n0.6km"] or line["labels"] == ["도로 구분 고르기\n0.6km"]
|
|
assert line["lh"] == 2 * 16 + 2 # 두 줄 높이
|
|
two = _node("legText([{road: '1차포장', km: 0.6}, {road: '비포장', km: null}])")
|
|
assert two == ["1차포장\n0.6km", "비포장\n-km"]
|
|
ui = _read(B08 + "UI_Milestone_Diagram.ts")
|
|
assert 'for (const row of label.split("\\n"))' in ui # 줄마다 tspan
|
|
assert "span.dataset.b08MilestoneLeg = `${ln.from}:${i}`;" in ui # 두 줄 다 그 구간
|
|
|
|
|
|
def test_연결_점_왼쪽_위_안쪽_기준점도_돌기_취소():
|
|
ui = _read(B08 + "UI_Milestone_Diagram.ts")
|
|
dot = ui[ui.index("const connectDot") :]
|
|
dot = dot[: dot.index("return dot;")]
|
|
# 왼쪽 위 안쪽 — 가운데 (10, 10) · 반지름 6 + 테두리 2 = 상자 안
|
|
assert "const cx = box.x + 10;" in dot and "const cy = box.y + 10;" in dot and "r: 6" in dot
|
|
assert (
|
|
'if (box.kind === "route" || box.kind === "base") g.append(connectDot(box, shift));' in ui
|
|
)
|
|
assert 'if (box.kind === "base" && makesLoop(fresh, box.id, t))' in dot
|
|
assert "if (id === box.id) return null;" in dot # 자기 = 취소
|
|
assert "route.to = t;" in dot and "route.linked = true;" in dot
|
|
css = _read(B08 + "UI_Milestone.css")
|
|
items = css[css.index(".b08-milestone__items {") :]
|
|
assert "font-size: 13px;" in items[: items.index("}")] # 항목 = 거래처 글
|
|
assert "items: 13" in _read(B08 + "Milestone_Layout.ts")
|
|
for name in ("Milestone_Layout.ts", "UI_Milestone_Diagram.ts"):
|
|
assert len(_read(B08 + name).splitlines()) <= 700, name
|