Files
Aislo/B05_Profile/B05_Profile_Route_Save_Parts.py
T
eomsangdonandClaude Opus 5.5 172ff8170b refactor(511-B B-6 정리): 운영 부름 0 파이썬 짝 걷음 · 남는 짝 머리에 남는 까닭 · 등록부 until 걷음 · 알려진 차 까닭 · 322_3 flow 빨강 고침
걷은 함수(운영 부름 0 · 전수 확인 = 화면 · 서버 · 체인 · 추천 엔진 · B07 · 시험)
- common_util/common_util_excavation.py 통째(126 줄 · pipe_trench_width_m · wall_trench_area_m2 · wall_trench_width_m · 서버 = Node excavation.trench)
- Engine_Grade_Carry.carry_edits(Node carry_seat 겉) · Route_Save_Parts.seat_route_moved(시험 헬퍼 helper_475_1_moved 로 옮김)
- Engine_Tilt.attach_underdrains · Engine_Structures_Wall.attach_wall_structures · load_wall_structures
- Engine_Grade_Alignment.tangent_elevation(부름 0) · station_key.dedupe_station_keys · loose_chainage
줄 수 전 → 후: excavation.py 126 → 0 · Grade_Carry 178 → 157 · Save_Parts 289 → 269 · Wall 221 → 197 · Tilt 115 → 112 · station_key 141 → 122 · Alignment 657 → 654

남는 짝(머리 「남는 까닭」 한 줄) — Grade · Grade_Profile · Alignment · CurveLaw(2~4 순위 폴백 · 추천) · NodeEdits · SampleExtend · Router_Design · Ranges · Engine_Design · Areas · cross_berm(DTM 아닌 지표면 · Node 못 부를 때 · B07 · 추천) · Tilt · Wall · Culvert(B04 B07 B08) · route_follow · structure_station · station_key · route_polyline(745 줄 · 까닭 = 등록부 R1 줄)

등록부 — until 칸 걷음(G1 = py 표본기 남김 · 까닭 note) · C6 pair = attach_underdrain_specs · F8 pair null
알려진 차 — golden KNOWN 셋 그대로 나타남(R7 표본 출처 · W0 저장 때 경고 · X2 입력 선 자릿수 · 까닭 고침) · underdrain 알려진 차 까닭 · 511_b5 KNOWN_ARC 그대로
계산 자리 창 — 45 줄 · 서버만 아닌 41(바꿀 수 있음 40 + F9) · 서버만 넷(G3 D1 D2 N1 줄마다 까닭) · 옛 까닭 「짝 모으는 중 · B-4」 글 걷음
test_322_3_flow 빨강 — 헬퍼가 @config/config_frontend 흉내(511-A design_calc_server import)
시험 기준 — 걷은 짝 거울 = 걷기 전 파이썬 값 고정(excavation · station_key) · Node carry_seat · attach_underdrain_specs · wall_specs_from

검증: 고친 대상 시험 97 파일 562 통과 · 21 건너뜀 · typecheck 0 · 700 줄 넘는 새 파일 없음 — 요구 320-4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:34:08 +09:00

270 lines
12 KiB
Python

"""노선 저장 길(346-3) 순수 부품 — 관 따라감 · 새 교차점 기본 관 · 측점 열쇠 ·
횡단 이월.
DB · 파일 없이 값만 받아 값을 돌려준다(시험이 그대로 부름). 차례 · 저장 · 되살림은
`B05_Profile_Route_Save.py`.
- 관 = 좌표가 정본 — 새 노선에 투영해 누가거리만 다시(`project_pipe_points` · 정수 m). 지우지 않음
(사용자 관 편집 지킴).
- 새 세류선 교차점(옛 노선 위에 없던 자리)에 관이 없으면 기본 관 하나(`_base_pipes` 결과를 받음) ·
같은 세류선 라벨 관(옮겼어도) · 지움 표지가 짝으로 잡은 교차점엔 안 더함(507-2 ·
`common_util_stream_crossing`).
- 관 최대 간격 보충 없음(369) — 간격 보충은 배수유역 [자동 제안]만.
- 측점 열쇠 = Node 측점 생성이 측점에 적은 `key`(511-B B-2 · `keyed_stations`) · 없으면(옛 파일 ·
DTM 아닌 지표면) `common_util_station_key.station_key` 한 벌(sub2 짝) — 일반 `grid:k` ·
관 `pipe:<id>` · 구조물 `structure:<id>` · 시종점 `end:BP` · `end:EP` ·
id 없으면 `at:<누가거리 0.01 m>`.
- 횡단 이월 = 같은 열쇠 측점에 `CARRY_INPUT_KEYS` 일곱 + `ROUTE_CARRY_KEYS`(사면 쉬프트 ·
소단 깊이 · 측점 폭 · 메모 · 상세 표 · 417) · 관 · 구조물 열쇠면
`STRUCTURE_ADJUST_KEYS` 도(구조물 조정은 구조물 따라) · 열쇠가 사라진 측점 수 =
「노선 밖 고침」.
"""
from __future__ import annotations
import copy
import math
from typing import Any, Sequence
from shapely.geometry import LineString, Point
from common_util.common_util_drainage_pipes import (
PIPE_SOURCE_STREAM,
PipePoint,
project_pipe_points,
)
from common_util.common_util_route_geometry import RouteVertex
from common_util.common_util_station_key import chainage_key as station_chainage_key
from common_util.common_util_station_key import (
curve_moves,
same_seat,
station_key,
station_seat,
)
from common_util.common_util_stream_crossing import claim_crossings, crossing_labels
from common_util.common_util_structure_station import plan_stations, snap_structure_station
from config.config_system import DRAINAGE_PIPE_MIN_SPACING_M
# 옛 노선에서 이만큼(m) 넘게 떨어진 자리 = 노선이 바뀐 자리(교차점 · 보충 대상).
MOVED_AWAY_M = 0.5
_SAME_M = 0.01
def vertices_of(xy: Sequence[Sequence[float]]) -> list[RouteVertex]:
"""평면 점열 → 누가거리 붙은 정점(z 0)."""
out: list[RouteVertex] = []
walked = 0.0
for index, point in enumerate(xy):
x, y = float(point[0]), float(point[1])
if index:
walked += math.hypot(x - out[-1].x, y - out[-1].y)
out.append(RouteVertex(x=x, y=y, z=0.0, chainage_m=walked))
return out
def follow_pipes(
old_pipes: list[PipePoint], vertices: list[RouteVertex]
) -> list[tuple[int, PipePoint]]:
"""관이 새 노선 좌표를 따라감 → [(옛 차례 · 더한 관 -1, 새 관)] 누가거리 순 · 옛 목록 그대로."""
pairs = [(index, copy.deepcopy(pipe)) for index, pipe in enumerate(old_pipes)]
for _, pipe in pairs:
project_pipe_points([pipe], vertices)
pairs.sort(key=lambda pair: pair[1].chainage_m)
return pairs
def _moved_away(old_line: LineString | None, x: float, y: float) -> bool:
return old_line is None or old_line.distance(Point(x, y)) > MOVED_AWAY_M
def add_crossing_pipes(
pairs: list[tuple[int, PipePoint]],
crossings: Sequence[Any],
old_xy: Sequence[Sequence[float]] | None,
total_m: float,
dismissed: Sequence[dict[str, Any]] = (),
) -> int:
"""옛 노선에 없던 세류선 교차점에 관이 없으면 기본 관(새 라벨)을 더함 · 더한 수."""
old_line = LineString(old_xy) if old_xy and len(old_xy) >= 2 else None
held = [pipe.stream_label for _, pipe in pairs if pipe.stream_label]
claimed = claim_crossings([*held, *(mark for mark in dismissed if "stream" in mark)], crossings)
labels = crossing_labels(crossings)
added = 0
for index, crossing in enumerate(crossings):
if index in claimed:
continue # 같은 세류선 라벨 관(옮겼어도) · 지운 자동 관(507-2)
if not _moved_away(old_line, crossing.x, crossing.y):
continue # 옛 교차점 — 사용자가 지운 관을 되살리지 않음
at = snap_structure_station(float(crossing.chainage_m), [total_m])
if any(abs(pipe.chainage_m - at) < DRAINAGE_PIPE_MIN_SPACING_M for _, pipe in pairs):
continue
pairs.append(
(
-1,
PipePoint(
chainage_m=at,
source=PIPE_SOURCE_STREAM,
x=crossing.x,
y=crossing.y,
stream_label=labels[index],
),
)
)
added += 1
pairs.sort(key=lambda pair: pair[1].chainage_m)
return added
def _facility_owners(
pipes: Sequence[PipePoint], structures: Sequence[Any]
) -> dict[float, tuple[str, str | None, str]]:
"""측점 자리(0.01 m) → (관 · 구조물, id, 역할 anchor · start · end). 같은 자리면 관이 먼저
(`plan_stations` 이름 규칙)."""
from B05_Profile.B05_Profile_Engine_Sections_Node import node_plan_stations
# 측점 계획 = Node(`stations.plan` · 511-B B-5) · 못 돌림 = 파이썬 `plan_stations`
groups = [([pipe], [], []) for pipe in pipes] + [([], [item], []) for item in structures]
planned = node_plan_stations(groups) or [plan_stations(*group) for group in groups]
marks_of = iter(planned)
owners: dict[float, tuple[str, str | None, str]] = {}
for pipe in pipes:
for mark in next(marks_of):
owners.setdefault(
round(float(mark["chainage_m"]), 2),
("pipe", getattr(pipe, "id", None), str(mark.get("role") or "anchor")),
)
for item in structures:
for mark in next(marks_of):
owners.setdefault(
round(float(mark["chainage_m"]), 2),
(
"structure",
getattr(item, "structure_id", None),
str(mark.get("role") or "anchor"),
),
)
return owners
def station_keys(
stations: Sequence[dict[str, Any]],
pipes: Sequence[PipePoint],
structures: Sequence[Any],
interval_m: float,
) -> dict[str, float]:
"""측점 열쇠(`common_util_station_key.station_key` 한 벌) → 누가거리.
시종점(맨 앞 · 맨 끝 = 관 · 구조물이 얹혀도 `end:BP/EP` · 화면 `buildStations` · B06
`stationKeysOf` 와 같음 · 452) > 관 · 구조물 자리(구간형 시작 · 끝 `:start` · `:end`) >
곡선 측점(`curve:` · 252-2) > 일반 격자(간격 배수인 것만) · id 없는 관 · 구조물은 `at:` ·
같은 열쇠가 둘이면 뒤엣것은 `at:`(B06 `stationKeysOf` 와 같음).
"""
owners = _facility_owners(pipes, structures)
rows = [row for row in stations if row.get("chainage_m") is not None]
values = [float(row["chainage_m"]) for row in rows]
out: dict[str, float] = {}
for index, chainage in enumerate(values):
owner = owners.get(round(chainage, 2))
if index == 0:
key = station_key("bp", chainage, interval_m)
elif index == len(values) - 1:
key = station_key("ep", chainage, interval_m)
elif owner is not None:
key = station_key(owner[0], chainage, interval_m, owner[1], role=owner[2])
elif rows[index].get("kind") == "curve":
key = station_key("curve", chainage, interval_m)
elif interval_m > 0 and abs(chainage - round(chainage / interval_m) * interval_m) < _SAME_M:
key = station_key("regular", chainage, interval_m)
else:
continue
if key in out:
key = f"at:{station_chainage_key(chainage)}"
out.setdefault(key, chainage)
return out
def keyed_stations(stations: Sequence[dict[str, Any]]) -> dict[str, float] | None:
"""측점이 지닌 열쇠(`key` · Node 측점 생성 출력 · 511-B B-2) → 누가거리 · 열쇠 없는 측점이 있으면
None(옛 파일 · DTM 아닌 지표면 = `station_keys` 로 셈)."""
rows = [row for row in stations if row.get("chainage_m") is not None]
if not rows or not all(isinstance(row.get("key"), str) for row in rows):
return None
out: dict[str, float] = {}
for row in rows:
out.setdefault(row["key"], float(row["chainage_m"]))
return out
def station_seats(
stations: Sequence[dict[str, Any]], keys: dict[str, float]
) -> dict[str, tuple[float, float, tuple[float, float] | None] | None]:
"""측점 열쇠 → 그 측점 자리(`station_seat` · 같은 누가거리 0.001 m 줄 · 모르면 None)."""
rows = {
round(float(row["chainage_m"]), 3): row
for row in stations
if isinstance(row, dict) and row.get("chainage_m") is not None
}
return {key: station_seat(rows.get(round(at, 3))) for key, at in keys.items()}
def carry_cross_inputs(
old_keys: dict[str, float],
new_keys: dict[str, float],
old_designs: dict[float, dict[str, Any]],
input_keys: Sequence[str],
adjust_keys: Sequence[str],
seat_keys: Sequence[str] = (),
seats: tuple[dict[str, Any], dict[str, Any]] | None = None,
) -> tuple[dict[float, dict[str, Any]], dict[float, dict[str, Any]], int]:
"""같은 열쇠 측점으로 횡단 입력을 넘김 → (입력{새 누가거리 0.001: 값}, 구조물 조정{…}, 버린 수).
`old_designs` = 옛 노선 행 {누가거리 0.001: design}. 버린 수 = 설계가 있던 옛 측점 중 새 노선에
같은 열쇠가 없는 것(「노선 밖 고침」) · MC 가 움직인 곡선 열쇠는 가까운 새 곡선 측점
(`curve_moves`). 자리 칸(`seat_keys` · 요구 275-3)은 옛 · 새 자리(`seats` = 열쇠 → 자리)가
같은(`same_seat`) 측점만 넘김 · 새 자리 = 기본값(TS `carryKeyedStates` 와 같은 규칙).
"""
moves = curve_moves(list(old_keys.items()), list(new_keys.items()))
old_seats, new_seats = seats or ({}, {})
inputs: dict[float, dict[str, Any]] = {}
adjusts: dict[float, dict[str, Any]] = {}
dropped = 0
for key, old_at in old_keys.items():
design = old_designs.get(round(old_at, 3))
if not isinstance(design, dict):
continue
new_key = moves.get(key, key)
new_at = new_keys.get(new_key)
if new_at is None:
dropped += 1
continue
seated = same_seat(old_seats.get(key), new_seats.get(new_key))
names = [n for n in input_keys if seated or n not in seat_keys]
names += [n for n in seat_keys if seated and n not in names]
carried = {name: design[name] for name in names if design.get(name) is not None}
if carried:
inputs[round(new_at, 3)] = carried
if key.startswith(("pipe:", "structure:")):
adjust = {name: design[name] for name in adjust_keys if design.get(name) is not None}
if adjust:
adjusts[round(new_at, 3)] = adjust
return inputs, adjusts, dropped
def route_moved_stations(
stations: Sequence[dict[str, Any]],
old_xy: Sequence[Sequence[float]],
new_xy: Sequence[Sequence[float]],
) -> list[float]:
"""노선 바뀜으로 옮긴 시설 측점 — 관 · 구조물 측점(`structure` 이름 있음) 중 옛 선과
같은 자리가 아닌 것(`line_matcher` 짝 없음 · 459). 같은 자리로 따라간 시설은 뺌(옛 높이
그대로). TS 짝 = `routeMovedStations`(B05_Profile/logic/B05_Profile_Pipeline_Grade.ts) ·
거울 시험 = test_459_route_seat_mirror.py."""
from B05_Profile.B05_Profile_Engine_Grade_Carry import line_matcher
match = line_matcher([tuple(p) for p in old_xy], [tuple(p) for p in new_xy])
return sorted(
float(station["chainage_m"])
for station in stations
if station.get("structure") and match(float(station["chainage_m"])) is None
)