"""E05 보고서 그림 — 서버 SVG(요구 347-5 · PLAN 534-3). PNG 는 `common_util_svg_png` 가 만듦.
section_svg(…) → 대표 단면(712 × 640) — 원지반 · 공사 뒤 지표 · 지층(무늬) · 성토(점) ·
우기 지하수위 · 조건별 임계 활동면(선 모양) · 노폭 · 비탈 높이 치수 ·
표고 · 거리 눈금 · 축척 막대 · 안전율 상자 · 지층 범례
band_svg(…) → 평면 위험 띠(684 × 320) — 노선 · 측점 2D 판정 색 · 3D 구간 띠
좌표 = B06 횡단 offset(좌 +) · 화면 왼쪽 = +offset
(B06 횡단도 관례 · `B06_Section_UI_Cross_View.ts`).
값 셈 없음 — 해석 결과(E03 · E04 저장본)를 그리기만.
"""
from __future__ import annotations
import math
from html import escape
from typing import Any
SECTION_W, SECTION_H = 712, 640
BAND_W, BAND_H = 684, 320
OK, NG, MUTED, INK = "#2E7D32", "#C00000", "#9E9E9E", "#222222"
CONDITION_COLORS = {"dry": "#D32F2F", "wet": "#1565C0", "seismic": "#7B1FA2"}
CONDITION_NAMES = {"dry": "건기", "wet": "우기", "seismic": "지진"}
SLIP_DASH = {"wet": "7 4", "seismic": "2 3"} # 건기 · 우기 임계면이 겹쳐도 둘 다 보이게
FONT = "font-family='Malgun Gothic, sans-serif'"
def _nice_step(span: float, count: int) -> float:
raw = max(span, 1e-6) / max(count, 1)
base = 10 ** math.floor(math.log10(raw))
return next(m * base for m in (1, 2, 5, 10) if m * base >= raw)
def _poly(points: list[tuple[float, float]], **attrs: str) -> str:
if len(points) < 2:
return ""
d = " ".join(f"{x:.1f},{y:.1f}" for x, y in points)
extra = " ".join(f"{k.replace('_', '-')}='{v}'" for k, v in attrs.items())
return f""
def _text(x: float, y: float, text: str, size: int = 11, anchor: str = "middle", **attrs) -> str:
extra = " ".join(f"{k.replace('_', '-')}='{v}'" for k, v in attrs.items())
return (
f"{escape(text)}"
)
def _interp(line: list[tuple[float, float]], x: float) -> float | None:
for (x0, z0), (x1, z1) in zip(line, line[1:]):
if min(x0, x1) - 1e-9 <= x <= max(x0, x1) + 1e-9:
return z0 if x1 == x0 else z0 + (z1 - z0) * (x - x0) / (x1 - x0)
return None
def _legend_rows(items: list[tuple[str, str]], width: float, size: int) -> list[list[int]]:
"""범례 항목 [(종류, 글)] → 줄마다 항목 번호(폭을 넘으면 다음 줄)."""
rows: list[list[int]] = [[]]
used = 0.0
for i, (_, label) in enumerate(items):
w = 30 + _text_w(label, size)
if rows[-1] and used + w > width:
rows.append([])
used = 0.0
rows[-1].append(i)
used += w
return rows
def _text_w(text: str, size: int) -> float:
"""글 폭 근사(px) — 한글 · 기호 = 글자 크기 · 그 밖 = 0.6 배."""
return sum(size if ord(ch) > 0x2000 else 0.6 * size for ch in text)
PATTERNS = (
""
""
""
""
""
""
)
STRATA_FILL = {"soil": "#F3E9D8", "rock": "url(#rockHatch)", "fill": "url(#fillDots)"}
def section_svg(
*,
title: str,
ground: list[tuple[float, float]],
surface: list[tuple[float, float]],
rock: list[tuple[float, float]] | None,
slips: list[dict[str, Any]],
road: tuple[float, float] | None,
heights: list[dict[str, Any]],
wet: bool,
strata: list[dict[str, str]] | None = None,
fs_box: list[dict[str, Any]] | None = None,
) -> str:
"""대표 단면 SVG(흑백 인쇄에도 읽힘 — 임계면 = 선 모양 · 지층 = 무늬).
ground · surface · rock = [(offset, z)] · slips = [{condition, F, points}] ·
road = (오른 끝 offset, 왼 끝 offset) · heights = [{x, z0, z1, label?}] 비탈 높이 치수 ·
strata = [{kind: soil | rock | fill, text}] 지층 범례 · fs_box = [{condition, F, required, ok}]
안전율 상자 · title = 빈 글이면 안 씀(보고서 쪽 제목이 있음).
"""
pad_l, pad_r, pad_t, pad_b = 58, 18, 30 if title else 22, 88
pts = [*ground, *surface, *(p for s in slips for p in s["points"])]
if not pts:
return _empty(SECTION_W, SECTION_H, title or "대표 단면")
xs = [p[0] for p in pts]
zs = [p[1] for p in pts]
x_lo, x_hi = min(xs), max(xs)
z_lo, z_hi = min(zs), max(zs)
z_lo -= 0.08 * (z_hi - z_lo + 1)
z_hi += 0.12 * (z_hi - z_lo + 1)
w, h = SECTION_W - pad_l - pad_r, SECTION_H - pad_t - pad_b
scale = min(w / max(x_hi - x_lo, 1e-6), h / max(z_hi - z_lo, 1e-6)) # 가로 · 세로 같은 축척
cx = pad_l + (w - (x_hi - x_lo) * scale) / 2
cz = pad_t + (h - (z_hi - z_lo) * scale) / 2
def sx(x: float) -> float:
return cx + (x_hi - x) * scale # +offset = 화면 왼쪽
def sz(z: float) -> float:
return cz + (z_hi - z) * scale
def screen(line: list[tuple[float, float]]) -> list[tuple[float, float]]:
return [(sx(x), sz(z)) for x, z in line]
def area(line: list[tuple[float, float]], y: float) -> str:
"""선과 y 사이를 닫은 다각형 점 글."""
body = screen(line)
return " ".join(f"{a:.1f},{b:.1f}" for a, b in [*body, (body[-1][0], y), (body[0][0], y)])
bottom, top = sz(z_lo), pad_t
out = [
f"")
return "".join(out)
def _fs_box(rows, surface, sx, sz, left: float, top: float, right: float) -> str:
"""안전율 상자 — 하늘 쪽(지표가 낮은 끝) 위 모서리 · 조건 · Fs · 기준 · 판정."""
width, line = 236, 19
height = 24 + line * len(rows)
# surface = offset 오름차순 · 화면 왼끝 = 큰 offset = 끝 점
low_left = bool(surface) and sz(surface[-1][1]) > sz(surface[0][1])
x = left + 6 if low_left else right - width - 6
y = top + 6
out = [
f"",
f"",
]
for cx, head, anchor in ((8, "조건", "start"), (78, "Fs", "end"), (132, "기준", "end")):
out.append(_text(x + cx, y + 16, head, 11, anchor, font_weight="bold", fill=INK))
out.append(_text(x + 196, y + 16, "판정", 11, font_weight="bold", fill=INK))
for k, r in enumerate(rows):
yy = y + 22 + line * (k + 1) - 5
name = CONDITION_NAMES.get(r["condition"], r["condition"])
f = "-" if r.get("F") is None else f"{r['F']:.2f}"
verdict = "-" if r.get("ok") is None else "O.K" if r["ok"] else "N.G"
color = INK if r.get("ok") is None else OK if r["ok"] else NG
out.append(_text(x + 8, yy, name, 11, "start", fill=INK))
out.append(_text(x + 78, yy, f, 11, "end", font_weight="bold", fill=INK))
out.append(_text(x + 132, yy, f"≥ {r['required']:.2f}", 11, "end", fill=INK))
out.append(_text(x + 196, yy, verdict, 11, font_weight="bold", fill=color))
return "".join(out)
def band_svg(
*,
title: str,
line: list[tuple[float, float, float]],
stations: list[dict[str, Any]],
sections: list[dict[str, Any]],
) -> str:
"""평면 위험 띠 SVG.
line = [(x, y, 누가)] 노선 · stations = [{x, y, ok: bool | None, label}] 2D 판정 ·
sections = [{start_m, end_m, ok: bool}] 3D 구간(노선 위 굵은 띠).
"""
pad = 24
xy = [(p[0], p[1]) for p in line] or [(s["x"], s["y"]) for s in stations]
if not xy:
return _empty(BAND_W, BAND_H, title)
xs = [p[0] for p in xy]
ys = [p[1] for p in xy]
w, h = BAND_W - 2 * pad, BAND_H - 2 * pad - 40
scale = min(w / max(max(xs) - min(xs), 1e-6), h / max(max(ys) - min(ys), 1e-6))
ox = pad + (w - (max(xs) - min(xs)) * scale) / 2
oy = pad + 22 + (h - (max(ys) - min(ys)) * scale) / 2
def sp(x: float, y: float) -> tuple[float, float]:
return ox + (x - min(xs)) * scale, oy + (max(ys) - y) * scale # 북 = 위
out = [
f"")
return "".join(out)
def _empty(width: int, height: int, title: str) -> str:
return (
f""
)