"""E05 보고서 그림 — 서버 SVG(요구 347-5 · PLAN 534-3). PNG 는 `common_util_svg_png` 가 만듦.
section_svg(…) → 대표 단면(712 × 533 · 4 : 3) — 원지반 · 공사 뒤 지표 · 암 경계 ·
우기 지하수위 ·
조건별 임계 활동면 · 노폭 · 비탈 높이 치수 · 표고 · 거리 눈금
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, 533
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": "지진"}
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 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,
) -> str:
"""대표 단면 SVG.
ground · surface · rock = [(offset, z)] · slips = [{condition, F, points}] ·
road = (오른 끝 offset, 왼 끝 offset) · heights = [{x, z0, z1}] 비탈 높이 치수.
"""
pad_l, pad_r, pad_t, pad_b = 56, 16, 34, 64
pts = [*ground, *surface, *(p for s in slips for p in s["points"])]
if not pts:
return _empty(SECTION_W, SECTION_H, title)
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]
out = [
f"")
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""
)