- 연결 점 = 거래처 · 기준점 상자 왼쪽 위 안쪽 · 기준점 = 시점 · 다른 기준점에 놓음(돌기면 취소 + 알림) - 기준점 상자 주소 · 주소 줄바꿈(최대 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
166 lines
7.2 KiB
Python
166 lines
7.2 KiB
Python
"""244-2 B08 거리이정표 다이어그램 — 방위 자리(8방 · 경위도 방위각 · 빈 방위 차례 ·
|
|
같은 방위 나란히) · 선 글(도로 구분 km) · 상자 누름 · [+] 항목 추가 길.
|
|
|
|
자리 계산 = `B08_Quantity_Milestone_Layout.ts` 를 node 로 돌림(화면 = 북 위 · SVG y 아래 = 남).
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B08_Quantity.B08_Quantity_Milestone_Calc import migrate_milestone # noqa: E402
|
|
|
|
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
|
|
|
|
|
def _layout(data, site=None, mid_km=0.75):
|
|
data = migrate_milestone(data) # 254 옛 꼴(base · via_base) → bases · to(옛 기준점 = b1)
|
|
src = (ROOT / "B08_Quantity" / "B08_Quantity_Milestone_Layout.ts").read_text(encoding="utf-8")
|
|
body = (
|
|
f"\nconst d = {json.dumps(data, ensure_ascii=False)};"
|
|
f"\nconsole.log(JSON.stringify(layoutMilestone(d, {json.dumps(site)}, {mid_km})));"
|
|
)
|
|
result = subprocess.run(
|
|
["node", "--experimental-strip-types", "--no-warnings",
|
|
"--input-type=module-typescript", "-e", IMPORTS.sub("", src) + body],
|
|
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
|
) # fmt: skip
|
|
assert result.returncode == 0, result.stderr
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
def _route(rid, name, dir=None, via=True, legs=None, lon=None, lat=None):
|
|
return {"id": rid, "name": name, "address": "", "via_base": via, "dir": dir, "lon": lon,
|
|
"lat": lat, "legs": legs or [],
|
|
"rows": [{"item": "", "dest": "start", "note": ""}]} # fmt: skip
|
|
|
|
|
|
def _data(routes, base=None):
|
|
return {
|
|
"site": {"mid_station_m": None, "mid_road": "비포장"},
|
|
"base": base or {"enabled": False, "name": "", "address": "", "dir": None, "legs": []},
|
|
"routes": routes,
|
|
}
|
|
|
|
|
|
def _center(layout, bid):
|
|
box = next(b for b in layout["boxes"] if b["id"] == bid)
|
|
return box["x"] + box["w"] / 2, box["y"] + box["h"] / 2
|
|
|
|
|
|
def test_공사지_컨테이너_시점_중점_선():
|
|
lay = _layout(_data([]))
|
|
kinds = {b["id"]: b["kind"] for b in lay["boxes"]}
|
|
assert kinds == {"site": "site", "start": "start", "mid": "mid"}
|
|
sx, _ = _center(lay, "start")
|
|
mx, _ = _center(lay, "mid")
|
|
assert sx < mx # 시점 왼 · 중점 오른 · 한 컨테이너 안
|
|
site = next(b for b in lay["boxes"] if b["id"] == "site")
|
|
for bid in ("start", "mid"):
|
|
b = next(x for x in lay["boxes"] if x["id"] == bid)
|
|
assert site["x"] <= b["x"] and b["x"] + b["w"] <= site["x"] + site["w"]
|
|
assert lay["lines"] == [
|
|
{**lay["lines"][0], "from": "start", "to": "mid", "labels": ["비포장\n0.75km"]} # 254 두 줄
|
|
]
|
|
|
|
|
|
def test_8방_자리_기준점_없음_공사지_기준():
|
|
lay = _layout(
|
|
_data([_route("e", "동쪽", "E"), _route("n", "북쪽", "N"), _route("sw", "남서", "SW")])
|
|
)
|
|
cx, cy = _center(lay, "site")
|
|
ex, ey = _center(lay, "e")
|
|
nx, ny = _center(lay, "n")
|
|
wx, wy = _center(lay, "sw")
|
|
# 254 선 길이 = km 비례(빈 km = 최소 100) — 공사지 테두리(가로 190 · 세로 53)에서 잼
|
|
assert ex > cx + 200 and abs(ey - cy) < 1
|
|
assert ny < cy - 150 and abs(nx - cx) < 1
|
|
assert wx < cx and wy > cy
|
|
line = next(ln for ln in lay["lines"] if ln["from"] == "e")
|
|
assert line["to"] == "start" # 기준점 없으면 시점으로
|
|
|
|
|
|
def test_기준점_기준_방위_선_글():
|
|
base = {"enabled": True, "name": "병곡리 968-31", "address": "", "dir": "W",
|
|
"legs": [{"road": "1차포장", "km": 0.6}, {"road": "비포장", "km": 0.8}]} # fmt: skip
|
|
lay = _layout(
|
|
_data(
|
|
[
|
|
_route("g", "거창군청", "N", legs=[{"road": "2차포장", "km": 23.8}]),
|
|
_route("k", "김천역", "E", via=False),
|
|
],
|
|
base,
|
|
), # fmt: skip
|
|
)
|
|
cx, cy = _center(lay, "site")
|
|
bx, by = _center(lay, "b1")
|
|
assert bx < cx - 200 and abs(by - cy) < 1 # 기준점 = 공사지 서쪽
|
|
gx, gy = _center(lay, "g")
|
|
assert abs(gx - bx) < 1 and gy < by - 200 # 거창군청 = 기준점 북쪽
|
|
kx, _ = _center(lay, "k")
|
|
assert kx > cx + 200 # 기준점 안 거침 = 공사지 동쪽
|
|
labels = {(ln["from"], ln["to"]): ln["labels"] for ln in lay["lines"]}
|
|
assert labels[("g", "b1")] == ["2차포장\n23.8km"] # 254 두 줄(도로 구분 / km)
|
|
assert labels[("b1", "start")] == ["1차포장\n0.6km", "비포장\n0.8km"]
|
|
assert labels[("k", "start")] == []
|
|
assert next(b for b in lay["boxes"] if b["id"] == "b1")["label"] == "병곡리 968-31"
|
|
|
|
|
|
def test_경위도_있으면_방위각이_8방보다_앞섬():
|
|
site = {"start_lonlat": [127.8, 35.7], "mid_lonlat": None}
|
|
lay = _layout(_data([_route("e", "동쪽 좌표", "N", lon=127.9, lat=35.7)]), site)
|
|
cx, cy = _center(lay, "site")
|
|
ex, ey = _center(lay, "e")
|
|
assert ex > cx + 200 and abs(ey - cy) < 1 # 고른 방위 N 보다 좌표(동) 먼저
|
|
|
|
|
|
def test_빈_방위_차례_같은_방위_나란히():
|
|
lay = _layout(
|
|
_data(
|
|
[_route("a", "가", "E"), _route("b", "나", "E"), _route("c", "다"), _route("d", "라")]
|
|
)
|
|
)
|
|
(ax, ay), (bx, by) = _center(lay, "a"), _center(lay, "b")
|
|
assert abs(ax - bx) < 1 and abs(ay - by) >= 60 # 같은 방위 = 옆으로 나란히
|
|
cx, cy = _center(lay, "c")
|
|
dx, dy = _center(lay, "d")
|
|
sx, sy = _center(lay, "site")
|
|
assert cy < sy - 150 and abs(cx - sx) < 1 # 고르지 않음 = 빈 방위 첫째(북)
|
|
assert (dx, dy) != (cx, cy) and dx < sx # 다음 빈 방위(북서)
|
|
boxes = [b for b in lay["boxes"] if b["kind"] == "route"]
|
|
for i, p in enumerate(boxes): # 업체 상자끼리 안 겹침
|
|
for q in boxes[i + 1 :]:
|
|
assert (
|
|
p["x"] + p["w"] <= q["x"] or q["x"] + q["w"] <= p["x"]
|
|
or p["y"] + p["h"] <= q["y"] or q["y"] + q["h"] <= p["y"]
|
|
) # fmt: skip
|
|
|
|
|
|
def test_화면_상자_누름_더하기_창():
|
|
ui = (ROOT / "B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts").read_text(encoding="utf-8")
|
|
# 244-4 — 상자 누름 = 모달(탭 이동 아님)
|
|
assert (
|
|
"openTab" not in ui
|
|
and "route: (id) => openPlaceModal(projectId, { add: false, id })," in ui
|
|
)
|
|
# 155 공사지 · 기준점 [+] 없앰 → 다이어그램 [요소 추가]
|
|
assert "plus(box" not in ui and 'label: "요소 추가"' in ui
|
|
assert "openPlaceModal(projectId, { add: true }, addRoute)" in ui
|
|
modal = (ROOT / "B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts").read_text(encoding="utf-8")
|
|
assert "openModal({" in modal
|
|
# 149 상자 하나 = 항목 하나 — [+ 줄] · 같은 이름 합침 없음
|
|
assert "same.rows.push" not in modal and '"+ 줄"' not in modal
|
|
page = (ROOT / "B08_Quantity/B08_Quantity_UI_Milestone.ts").read_text(encoding="utf-8")
|
|
assert "createDiagram(projectId, (init) => addRoute(projectId, init))" in page
|
|
assert "children: [diagram.root, none, table.root]" in page and "diagram.refresh();" in page
|
|
for name in ("Milestone_Layout.ts", "UI_Milestone_Diagram.ts"):
|
|
assert (
|
|
len((ROOT / f"B08_Quantity/B08_Quantity_{name}").read_text("utf-8").splitlines()) <= 700
|
|
)
|