Files
Aislo/B06_Section/B06_Section_Engine_Attach.py
T
eomsangdonandClaude Opus 5.5 37a738aff4 fix(458 B06 노선 바뀜): 주인 측점 거리 상한 0.51 m · 시종점 밖 시설 안 얹음 · 갈아끼움 grid 지움 · 늘 다시 얹기 · 이름 = 계획 제목
- 관 세트 주인 측점 = 0.51 m 안 가장 가까운 측점(TS attachCulvertSets ↔ 파이썬 attach_culvert_specs) · 넘으면 연동 이웃에도 안 얹고 missed
- 형상 얹기 한 벌(attachShapes ↔ attach_shapes) — 시종점 측점 자리인데 좌표가 노선 진행 반대쪽 0.51 m 넘게 바깥 = 「노선 밖」 · 안 얹음 · 안 얹은 것 돌려줌(서버 = 로그 경고)
- 측점 갈아끼움 — 격자 아닌 자리면 앞 횡단 grid 표시 지움 · 갈아끼움 뒤 가게 초안 없어도 늘 다시 얹음 · 카드 · 종단 측점 이름 = 계획 제목 · 안 얹은 시설 알림(바뀐 때 한 번)
- 시험: test_458_swap_attach(재현 A · D) · test_323_4 거울 moved 경우(상한 · 시종점 밖 · 연동 폭) · test_299 같은 키 견본 = 주인 0.51 m 안으로 · 273_2 흉내 돌려줌

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:44:41 +09:00

153 lines
6.7 KiB
Python

"""형상 얹기 한 벌(323-4) — 측점(횡단 dict) + 현황(관 · 구조물) + 구조물 표 → 그 측점 그림 자료.
구조물 열쇠(`SHAPE_KEYS` · 빈칸 경고 `blank`)를 걷고 차례대로 다시 얹음 — 관 세트 → 벽 → 포장 → 맹암거 →
빈칸 경고. 고르기 = 표 형상(322-1). 서버 상세 읽기(`_read_section_detail`) · 재생성 응답이 이것 하나를 씀.
TS 짝 = `common_util/common_util_structure_attach.ts` `attachShapes`(화면 = 초안 ?? 저장분 현황) ·
거울 시험 = `resources/tester/test_323_4_attach_mirror.py`.
"""
from __future__ import annotations
import logging
import math
from pathlib import Path
from typing import Any
from B05_Profile.B05_Profile_Structures_Repository import load_structures
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B06_Section.B06_Section_Engine_Culvert import (
attach_culvert_specs,
culvert_sets_from,
pipe_points_file,
)
from B06_Section.B06_Section_Engine_Structures_Wall import attach_wall_specs, wall_specs_from
from B06_Section.B06_Section_Engine_Tilt import attach_underdrain_specs, underdrains_from
from common_util.common_util_drainage_pipes import read_pipe_points_file
from common_util.common_util_structure_catalog import catalog_row, field_name, type_ids_with
from common_util.common_util_structure_pavement import (
attach_pavement_specs,
pavement_specs_from,
)
from common_util.common_util_structure_station import (
PIPE_MATCH_M,
SAME_STATION_M,
blank_keys,
plan_stations,
)
logger = logging.getLogger(__name__)
#: 측점 그림 자료 열쇠 — TS `common_util_structure_shape_keys.SHAPE_KEYS` 짝.
SHAPE_KEYS = ("culvert", "ford", "box", "ford_pavement", "revetment", "pavement", "underdrain")
def _blank_labels(type_id: str, options: dict | None, types: dict[str, Any]) -> list[str]:
"""빈 필수 칸 → 「종류 이름 칸 이름」(TS `blankLabels` 짝)."""
name = (catalog_row(type_id) or {}).get("name") or type_id
ordered = [types[type_id]] if type_id in types else []
ordered += list(types.values())
out = []
for key in blank_keys(type_id, options):
label = next(
(o.label for t in ordered for o in t.options if o.key == key and o.label), None
)
out.append(f"{name} {label or field_name(key) or key}")
return out
def _attach_blanks(sections: list[dict[str, Any]], pipes: list[Any], structures: list[Any]) -> None:
"""빈칸 경고 — 측점 계획 자리마다 덮는 관 · 구조물의 빈 필수 칸(TS `attachBlanks` 짝)."""
types = structure_type_map()
owners: dict[str, tuple[str, dict | None]] = {}
for pipe in pipes:
owner = getattr(pipe, "id", None) or f"pipe-{float(pipe.chainage_m):.2f}"
owners[owner] = (pipe.facility or "pipe", pipe.options)
for item in structures:
if item.structure_id:
owners[item.structure_id] = (item.type_id, item.options)
for station in plan_stations(pipes, structures):
if not station["blank"]:
continue
labels: list[str] = []
for owner in station["owners"]:
hit = owners.get(owner)
for label in _blank_labels(hit[0], hit[1], types) if hit else []:
if label not in labels:
labels.append(label)
for section in sections:
at = section.get("chainage_m")
if isinstance(at, (int, float)) and abs(at - station["chainage_m"]) < SAME_STATION_M:
section["blank"] = labels
def _finite(value: Any) -> bool:
return isinstance(value, (int, float)) and math.isfinite(value)
def _outside_route(pipe: Any, sections: list[dict[str, Any]]) -> bool:
"""시종점 밖으로 밀린 관인가(458 · TS `outsideRoute` 짝) — 시종점 측점 자리인데 좌표가 노선 진행
반대쪽으로 `PIPE_MATCH_M` 넘게 바깥(옆 거리는 안 봄 · 좌표 없으면 아님)."""
placed = sorted(
(
s
for s in sections
if all(_finite(s.get(k)) for k in ("chainage_m", "center_x", "center_y"))
),
key=lambda s: s["chainage_m"],
)
x, y = getattr(pipe, "x", None), getattr(pipe, "y", None)
if len(placed) < 2 or not (_finite(x) and _finite(y)):
return False
for end, nxt in ((placed[0], placed[1]), (placed[-1], placed[-2])):
if abs(end["chainage_m"] - float(pipe.chainage_m)) >= PIPE_MATCH_M:
continue
dx, dy = nxt["center_x"] - end["center_x"], nxt["center_y"] - end["center_y"]
span = math.hypot(dx, dy)
if (
span > 0
and ((x - end["center_x"]) * dx + (y - end["center_y"]) * dy) / span <= -PIPE_MATCH_M
):
return True
return False
def attach_shapes(
sections: list[dict[str, Any]], pipes: list[Any], structures: list[Any]
) -> dict[str, list[float]]:
"""측점마다 그림 자료를 걷고 현황으로 다시 얹음(TS `attachShapes` 짝 · 제자리 고침) ·
시종점 밖 관 · 주인 측점 먼 관은 안 얹고 돌려줌(458 · `{"outside", "far"}`)."""
for section in sections:
for key in (*SHAPE_KEYS, "blank"):
section.pop(key, None)
misses: dict[str, list[float]] = {"outside": [], "far": []}
kept = []
for pipe in pipes:
if _outside_route(pipe, sections):
misses["outside"].append(float(pipe.chainage_m))
else:
kept.append(pipe)
pipes = kept
attach_culvert_specs(sections, culvert_sets_from(pipes), misses["far"])
# 관 세트가 선 측점은 벽을 안 얹음(`attach_wall_specs`) — 차례 = 화면과 같음
attach_wall_specs(sections, wall_specs_from(structures))
attach_pavement_specs(
sections,
pavement_specs_from([s.model_dump() for s in structures], type_ids_with(shape="pavement")),
)
attach_underdrain_specs(sections, underdrains_from(structures))
_attach_blanks(sections, pipes, structures)
if misses["outside"] or misses["far"]:
logger.warning("B06 시설 그림 안 얹음 — 노선 밖 %s · 측점 멂 %s", *misses.values())
return misses
def attach_saved_shapes(project_root: Path, sections: list[dict[str, Any]]) -> None:
"""저장분 현황(관 지점 · 구조물 정본)으로 얹음 — 구조물 정본을 못 읽으면 관만(횡단 조회는 이어 감)."""
pipes = read_pipe_points_file(pipe_points_file(project_root))
try:
structures = load_structures(str(project_root))[1]
except Exception: # noqa: BLE001 — 정본을 못 읽어도 횡단 조회는 이어 간다
logger.exception("B06 구조물 정본을 읽지 못했습니다 (관만 얹음)")
structures = []
attach_shapes(sections, pipes, structures)