- 배수유역도 = 기본 종류(양식 watershed · 1/6,000 한 장 · 조각 없음) · 도면 id plan_watershed · × 지우기 · 「새 종류」 양식 고르기로 다시 · 옛 폴더에 한 번만 더함 - 평면도 칸 종류 · 양식 고르기 = 공용 createSelectField(compact) - IP 표 첫 줄 맨 위 · 왼 → 오 · 위 → 아래(묶음 왼쪽 아래 기준 그대로) - 용지도 필지 정보 글 3.6(상수 하나) · 상자 밑 등고선 · 계류 · 필지 · 읍면동 · 시군구 선 잘라 냄(clip_lines 일반화) - 시험: 기본 종류 · 유역 한 장 · 지우기 · 다시 만들기 · IP 표 두 줄 · 상자 안 선 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
222 lines
8.8 KiB
Python
222 lines
8.8 KiB
Python
"""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) -> 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,
|
||
) -> 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)
|
||
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
|