"""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"", f"", _text(SECTION_W / 2, 20, title, 13, font_weight="bold", fill=INK), ] # 눈금(표고 · 거리) step_z = _nice_step(z_hi - z_lo, 6) z = math.ceil(z_lo / step_z) * step_z while z <= z_hi: y = sz(z) out.append( f"" ) out.append(_text(pad_l - 6, y + 4, f"{z:g}", 10, "end", fill="#666666")) z += step_z step_x = _nice_step(x_hi - x_lo, 8) x = math.ceil(x_lo / step_x) * step_x base_y = SECTION_H - pad_b + 14 while x <= x_hi: out.append(_text(sx(x), base_y, f"{x:g}", 10, fill="#666666")) x += step_x out.append(_text(pad_l - 6, pad_t - 8, "EL. (m)", 10, "end", fill="#666666")) out.append( _text(SECTION_W - pad_r, base_y + 14, "offset (m) · 좌 +", 10, "end", fill="#666666") ) # 지반 · 지층 if ground: bottom = sz(z_lo) body = screen(ground) out.append( "" ) if rock: rb = screen(rock) out.append( "" ) out.append(_poly(rb, stroke="#795548", stroke_width="1.2", stroke_dasharray="6 3")) out.append(_poly(screen(ground), stroke="#8D6E63", stroke_width="1.4")) out.append(_poly(screen(surface), stroke=INK, stroke_width="2")) if wet and surface: # 우기 지하수위 = 공사 뒤 지표(KDS 11 70 05 표 4.3-3) — ▽ 표시 for frac in (0.2, 0.8): px, pz = surface[int(frac * (len(surface) - 1))] a, b = sx(px), sz(pz) - 4 out.append( f"" ) # 임계 활동면 for slip in slips: color = CONDITION_COLORS.get(slip["condition"], NG) out.append(_poly(screen(slip["points"]), stroke=color, stroke_width="2.2")) # 치수 — 노폭 · 비탈 높이 if road: r_off, l_off = road zr, zl = _interp(surface, r_off), _interp(surface, l_off) if zr is not None and zl is not None: y = min(sz(zr), sz(zl)) - 18 a, b = sx(l_off), sx(r_off) out.append( f"" ) out.append(_text((a + b) / 2, y - 4, f"노폭 {abs(l_off - r_off):.2f} m", 10, fill=INK)) for dim in heights: if abs(dim["z1"] - dim["z0"]) < 0.05: continue a = sx(dim["x"]) y0, y1 = sz(dim["z0"]), sz(dim["z1"]) out.append( f"" ) left = a < SECTION_W / 2 # 화면 왼쪽 치수 = 글은 더 왼쪽(비탈과 안 겹침) label = f"H {abs(dim['z1'] - dim['z0']):.2f} m" out.append( _text( a - 4 if left else a + 4, (y0 + y1) / 2, label, 10, "end" if left else "start", fill=INK, ) ) out.insert( 2, "" "", ) # 범례 legend = [("#8D6E63", "원지반", ""), (INK, "공사 뒤 지표", "")] if rock: legend.append(("#795548", "암 경계", "6 3")) for slip in slips: name = CONDITION_NAMES.get(slip["condition"], slip["condition"]) f = slip.get("F") legend.append( ( CONDITION_COLORS.get(slip["condition"], NG), f"{name} 임계면 Fs {f:.2f}" if f else name, "", ) ) if wet: legend.append(("#1565C0", "▽ 우기 지하수위 = 지표", "")) lx, ly = pad_l, SECTION_H - 22 for color, label, dash in legend: dash_attr = f" stroke-dasharray='{dash}'" if dash else "" out.append( f"" ) out.append(_text(lx + 22, ly + 4, label, 10, "start", fill=INK)) lx += 34 + sum(10 if ord(ch) > 0x2000 else 6 for ch in label) # 한글 · 기호 폭 out.append("") 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"", f"", _text(BAND_W / 2, 18, title, 13, font_weight="bold", fill=INK), ] out.append(_poly([sp(x, y) for x, y in xy], stroke="#BDBDBD", stroke_width="3")) for sec in sections: part = [sp(x, y) for x, y, ch in line if sec["start_m"] - 1e-6 <= ch <= sec["end_m"] + 1e-6] color = OK if sec["ok"] else NG out.append(_poly(part, stroke=color, stroke_width="12", stroke_opacity="0.35")) for st in stations: a, b = sp(st["x"], st["y"]) color = MUTED if st["ok"] is None else OK if st["ok"] else NG out.append(f"") for st in (stations[0], stations[-1]) if stations else (): a, b = sp(st["x"], st["y"]) out.append(_text(a, b - 7, st["label"] or "", 10, fill=INK)) lx, ly = pad, BAND_H - 14 for color, label, width in ( (OK, "2D O.K", 0), (NG, "2D N.G", 0), (OK, "3D 구간 O.K", 12), (NG, "3D 구간 N.G", 12), ): if width: out.append( f"" ) else: out.append(f"") out.append(_text(lx + 26, ly, label, 10, "start", fill=INK)) lx += 120 if not sections: out.append(_text(BAND_W - pad, ly, "3D 해석 전", 10, "end", fill=MUTED)) out.append("") return "".join(out) def _empty(width: int, height: int, title: str) -> str: return ( f"" f"" + _text(width / 2, height / 2, f"{title} — 자료 없음", 13, fill=MUTED) + "" )