"""B07 평면도 IP 표 — 곡선표에서 BP · IP · EP 자료를 셈해 장 도곽 왼쪽 아래 표로(PLAN 94장). IP 는 곡선이 있는 것만 · 번호는 평면도 「IPn」 과 같은 곡선표 차례다. IA 는 저장 내각(소수 2자리) 대신 곡선 자기 접선(시점→교점 · 교점→종점 방향각)으로 다시 셈하고, TL · CL · SL 은 R 과 그 IA 로 셈한다. 좌표는 측량 관례대로 X = 북(y) · Y = 동(x). 표 자리(종이 mm)는 자료를 만들 때 정한다 — 표 밑 등고선 · 노선을 같은 자리로 잘라 내야 해서다. """ import math from typing import Any import numpy as np from shapely.geometry import LineString, MultiLineString from shapely.geometry import box as rect from shapely.ops import unary_union from shapely.prepared import prep from B05_Profile.B05_Profile_Engine_Sections_Core import ( DESIGN_CURVE_MATCH_TOLERANCE_M, _project_chainage, ) from B07_DesignDetail.B07_DesignDetail_Engine_Cad import table_entity from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import _FONT_SIZE, MM LAYER_ID = "b07-plan-ip_table" LINE_COLOR = "#ff4d4d" # 예시 표 — 선 빨강 · 글 파랑 TEXT_COLOR = "#4d7dff" _COLUMNS = (7.0, 6.0, 23.0) # [머리 | X·Y | 값] 종이 mm _ROW_H = 5.0 _MARGIN = 5.0 # 도곽 왼쪽 아래 모서리에서 띄움 _LINE_GAP = 3.0 # 한 줄 넘쳐 위로 올린 줄 사이 _IP_ROWS = 8 # IP · CO(X · Y) · IA · R · TL · CL · SL _END_ROWS = 3 # BP · EP = IP · CO 만 _VALUE_ROWS = (("I A", "ia"), ("R", "r"), ("T L", "tl"), ("C L", "cl"), ("S L", "sl")) _CLIPPED = ("contour", "stream", "road_edge", "fill_outline", "cut_outline", "centerline") def intersection_angle(start: Any, apex: Any, end: Any) -> float: """교각 IA(도) — 시점→교점 · 교점→종점 방향각 차.""" first = math.atan2(apex[1] - start[1], apex[0] - start[0]) second = math.atan2(end[1] - apex[1], end[0] - apex[0]) return abs(math.degrees((second - first + math.pi) % (2 * math.pi) - math.pi)) def curve_values(radius: float, ia_deg: float) -> dict[str, float]: """TL = R·tan(IA/2) · CL = R·IA(rad) · SL = R·(sec(IA/2) − 1).""" half = math.radians(ia_deg) / 2.0 return { "tl": radius * math.tan(half), "cl": radius * 2.0 * half, "sl": radius * (1.0 / math.cos(half) - 1.0), } def dms(degrees: float) -> str: """도-분-초 「94-59-58.38」 — 분 · 초 두 자리 · 초는 0.01″ 로 반올림해 60.00 이면 올림.""" hundredths = round(degrees * 360000) whole, rest = divmod(hundredths, 360000) minutes, seconds = divmod(rest, 6000) return f"{whole}-{minutes:02d}-{seconds // 100:02d}.{seconds % 100:02d}" def ip_blocks( curves: list[dict[str, Any]], route: list[tuple[float, float, float]], number: int, total: int, start_m: float, end_m: float, ) -> list[dict[str, Any]]: """그 장의 표 묶음 [BP?, IP…, EP?] — 곡선 가운데 누가거리가 그 장 [시작, 끝) 안인 것만. 장 경계 측점은 두 장이 나눠 가지므로 끝은 열어 둔다(마지막 장만 닫음) — 한 IP 는 한 장에만. 노선에서 5 m 넘게 떨어진 옛 곡선은 버린다(B05 곡선 확폭과 같은 허용). """ if len(route) < 2: return [] points = np.asarray([(x, y) for _c, x, y in route], dtype=np.float64) chainage = np.asarray([c for c, _x, _y in route], dtype=np.float64) blocks: list[dict[str, Any]] = [] if number == 1: blocks.append({"name": "BP", "x": route[0][1], "y": route[0][2]}) for index, curve in enumerate(curves, start=1): start, apex, end = curve.get("start"), curve.get("apex"), curve.get("end") radius = curve.get("radius_m") if not (start and apex and end) or not isinstance(radius, (int, float)): continue start_gap, start_c = _project_chainage(points, chainage, tuple(start[:2])) end_gap, end_c = _project_chainage(points, chainage, tuple(end[:2])) if max(start_gap, end_gap) > DESIGN_CURVE_MATCH_TOLERANCE_M: continue middle = (start_c + end_c) / 2.0 if (number > 1 and middle < start_m) or (number < total and middle >= end_m): continue ia = intersection_angle(start, apex, end) blocks.append( { "name": str(index), "x": float(apex[0]), "y": float(apex[1]), "ia": ia, "r": float(radius), **curve_values(float(radius), ia), } ) if number == total: blocks.append({"name": "EP", "x": route[-1][1], "y": route[-1][2]}) return blocks def _place(blocks: list[dict[str, Any]], width_mm: float) -> None: """묶음마다 표 왼쪽 위 자리(종이 mm) `origin` — 왼쪽 → 오른쪽 · 위 → 아래(첫 줄 맨 위). 묶음 전체는 도곽 왼쪽 아래 기준 — 줄이 늘면 위로 쌓이고 마지막 줄이 맨 아래다. """ block_w = sum(_COLUMNS) per_line = max(1, int((width_mm - 2 * _MARGIN) // block_w)) lines = -(-len(blocks) // per_line) for order, block in enumerate(blocks): line, column = divmod(order, per_line) top = _MARGIN + (lines - 1 - line) * (_IP_ROWS * _ROW_H + _LINE_GAP) + _IP_ROWS * _ROW_H block["origin"] = [_MARGIN + column * block_w, top] block["rows"] = _END_ROWS if "ia" not in block else _IP_ROWS def clip_lines( pieces: dict[str, dict[str, Any]], rects: list[tuple[float, float, float, float]], names: tuple[str, ...], ) -> None: """상자들(사업지 m · 최소 x · y · 최대 x · y) 밑 선 조각 `names` 를 상자 밖으로 잘라 낸다.""" if not rects: return area = unary_union([rect(*bounds) for bounds in rects]) inside = prep(area) for piece in names: lines = (pieces.get(piece) or {}).get("lines") if not lines: continue kept: list[list[list[float]]] = [] for points in lines: if len(points) < 2 or not inside.intersects(LineString(points)): kept.append(points) continue rest = LineString(points).difference(area) parts = rest.geoms if isinstance(rest, MultiLineString) else [rest] kept += [ [[round(x, 4), round(y, 4)] for x, y in part.coords] for part in parts if isinstance(part, LineString) and len(part.coords) >= 2 ] pieces[piece]["lines"] = kept def _clip( pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box: Any, mm: float = MM ) -> None: """표 칸 밑 선 조각(등고선 · 계류 · 노견선 · 외곽선 · 중심선)을 표 밖으로 잘라 낸다.""" min_x, min_y = box[0], box[1] width = sum(_COLUMNS) / mm rects = [ ( min_x + b["origin"][0] / mm, min_y + b["origin"][1] / mm - b["rows"] * _ROW_H / mm, min_x + b["origin"][0] / mm + width, min_y + b["origin"][1] / mm, ) for b in blocks ] clip_lines(pieces, rects, _CLIPPED) def ip_table_piece( pieces: dict[str, dict[str, Any]], curves: list[dict[str, Any]], route: list[tuple[float, float, float]], box: tuple[float, float, float, float], chunk: dict[str, Any], total: int, mm: float = MM, ) -> dict[str, Any]: """그 장 IP 표 조각 {blocks} — 자리를 정하고 같은 자리 밑 선 조각을 `pieces` 에서 잘라 낸다.""" blocks = ip_blocks( curves, route, int(chunk["number"]), total, float(chunk["start_m"]), float(chunk["end_m"]), ) _place(blocks, (box[2] - box[0]) * mm) if blocks: _clip(pieces, blocks, box, mm) return {"blocks": blocks} def ip_table_entities(drawing_id: str, data: dict[str, Any]) -> list[dict[str, Any]]: """묶음마다 표 하나 — 행 IP · CO(X · Y 2행 병합) · IA · R · TL · CL · SL(값 2 · 3열 병합).""" out = [] for block in data.get("blocks") or []: cells: list[list[dict[str, Any] | None]] = [ [{"text": "I P"}, {"text": block["name"], "colSpan": 2}, None], [{"text": "C O", "rowSpan": 2}, {"text": "X"}, {"text": f"{block['y']:.5f}"}], [None, {"text": "Y"}, {"text": f"{block['x']:.5f}"}], ] if "ia" in block: for label, key in _VALUE_ROWS: value = dms(block[key]) if key == "ia" else f"{block[key]:.5f}" cells.append([{"text": label}, {"text": value, "colSpan": 2}, None]) out.append( table_entity( f"{drawing_id}:ip_table:{block['name']}", tuple(block["origin"]), list(_COLUMNS), [_ROW_H] * len(cells), cells, LAYER_ID, LINE_COLOR, _FONT_SIZE, TEXT_COLOR, ) ) return out