refactor(346-3): 노선 저장 횡단 이월 측점 열쇠 = common_util_station_key 한 벌

- station_keys 가 station_key(sub2 짝)를 부름 · 관 pipe:<id>(id 없으면 at:<0.01 m>) · 구조물 structure:<id> · 같은 열쇠 둘째는 at:(B06 stationKeysOf 와 같음)
- 파일 차례 관 열쇠 없앰 · 시험 test_346_3_route_save 고쳐 씀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-16 11:59:38 +09:00
co-authored by Claude Opus 5.5
parent 6757d1dcb8
commit 3995e91a1c
3 changed files with 59 additions and 47 deletions
+3 -3
View File
@@ -284,10 +284,10 @@ async def _carry_cross(
new_long = _read_json(project_root / relative)
_, new_structures = load_structures(str(project_root))
old_keys = station_keys(
old_long.get("stations") or [], list(enumerate(old_pipes)), old_structures, interval_m
old_keys = station_keys(old_long.get("stations") or [], old_pipes, old_structures, interval_m)
new_keys = station_keys(
new_long.get("stations") or [], [pipe for _, pipe in pairs], new_structures, interval_m
)
new_keys = station_keys(new_long.get("stations") or [], pairs, new_structures, interval_m)
designs = {
round(float(row["chainage_m"]), 3): row["design"]
for row in old_designs
+42 -35
View File
@@ -8,9 +8,9 @@ DB · 파일 없이 값만 받아 값을 돌려준다(시험이 그대로 부름
(사용자 관 편집 지킴).
- 새 세류선 교차점(옛 노선 위에 없던 자리)에 관이 없으면 기본 관 하나(`_base_pipes` 결과를 받음).
- 관 최대 간격 보충 = 옛 노선에서 벗어난 자리의 **오목 지점**(지반 종단 낮은 곳)에만 — 격자 없음.
- 측점 열쇠(계약 `design_pipeline_types.ts` 머리) = 일반 `grid:k`(누가거리 ÷ 측점 간격 반올림) ·
관 `pipe:<id>` · 구조물 `structure:<id>`(구간 시작 · 끝은 `:start` · `:end`) · 시종점
`end:BP` · `end:EP`. 관 id = 저장 파일 차례(노선 저장 한 번 안에서 옛 · 새가 같음).
- 측점 열쇠 = `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` 일곱 · 관 · 구조물 열쇠면
`STRUCTURE_ADJUST_KEYS` 도(구조물 조정은 구조물 따라) · 열쇠가 사라진 측점 수 =
「노선 밖 고침」.
@@ -32,6 +32,8 @@ from common_util.common_util_drainage_pipes import (
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 station_key
from common_util.common_util_structure_station import plan_stations, snap_structure_station
from config.config_system import DRAINAGE_PIPE_MAX_SPACING_M, DRAINAGE_PIPE_MIN_SPACING_M
@@ -59,7 +61,7 @@ def vertices_of(xy: Sequence[Sequence[float]]) -> list[RouteVertex]:
def follow_pipes(
old_pipes: list[PipePoint], vertices: list[RouteVertex]
) -> list[tuple[int, PipePoint]]:
"""관이 새 노선 좌표를 따라감 → [(관 id = 옛 차례, 새 관)] 누가거리 순 · 옛 목록 그대로."""
"""관이 새 노선 좌표를 따라감 → [(옛 차례 · 더한 관 -1, 새 관)] 누가거리 순 · 옛 목록 그대로."""
pairs = [(index, copy.deepcopy(pipe)) for index, pipe in enumerate(old_pipes)]
for _, pipe in pairs:
project_pipe_points([pipe], vertices)
@@ -145,49 +147,54 @@ def fill_concave_pipes(
added += 1
def _facility_keys(
pipes: Sequence[tuple[int, PipePoint]], structures: Sequence[Any]
) -> dict[float, str]:
"""측점 자리(0.01 m) → 관 · 구조물 열쇠. 같은 자리면 관이 먼저(`plan_stations` 이름 규칙)."""
keys: dict[float, str] = {}
def put(marks: list[dict[str, object]], head: str) -> None:
for mark in marks:
role = str(mark.get("role") or "anchor")
keys.setdefault(
round(float(mark["chainage_m"]), 2), head if role == "anchor" else f"{head}:{role}"
def _facility_owners(
pipes: Sequence[PipePoint], structures: Sequence[Any]
) -> dict[float, tuple[str, str | None]]:
"""측점 자리(0.01 m) → (관 · 구조물, id). 같은 자리면 관이 먼저(`plan_stations` 이름 규칙)."""
owners: dict[float, tuple[str, str | None]] = {}
for pipe in pipes:
for mark in plan_stations([pipe], []):
owners.setdefault(
round(float(mark["chainage_m"]), 2), ("pipe", getattr(pipe, "id", None))
)
for pipe_id, pipe in pipes:
if pipe_id >= 0:
put(plan_stations([pipe], []), f"pipe:{pipe_id}")
for item in structures:
if getattr(item, "structure_id", None):
put(plan_stations([], [item]), f"structure:{item.structure_id}")
return keys
for mark in plan_stations([], [item]):
owners.setdefault(
round(float(mark["chainage_m"]), 2),
("structure", getattr(item, "structure_id", None)),
)
return owners
def station_keys(
stations: Sequence[dict[str, Any]],
pipes: Sequence[tuple[int, PipePoint]],
pipes: Sequence[PipePoint],
structures: Sequence[Any],
interval_m: float,
) -> dict[str, float]:
"""측점 열쇠 → 누가거리. 관 · 구조물 자리 > 시종점 > 일반 격자(간격 배수인 것만)."""
facility = _facility_keys(pipes, structures)
"""측점 열쇠(`common_util_station_key.station_key` 한 벌) → 누가거리.
관 · 구조물 자리 > 시종점 > 일반 격자(간격 배수인 것만) · id 없는 관 · 구조물은 `at:` ·
같은 열쇠가 둘이면 뒤엣것은 `at:`(B06 `stationKeysOf` 와 같음).
"""
owners = _facility_owners(pipes, structures)
values = [float(row["chainage_m"]) for row in stations if row.get("chainage_m") is not None]
out: dict[str, float] = {}
for index, chainage in enumerate(values):
key = facility.get(round(chainage, 2))
if key is None and index == 0:
key = "end:BP"
elif key is None and index == len(values) - 1:
key = "end:EP"
elif key is None and interval_m > 0:
k = round(chainage / interval_m)
key = f"grid:{k}" if abs(chainage - k * interval_m) < _SAME_M else None
if key is not None:
out.setdefault(key, chainage)
owner = owners.get(round(chainage, 2))
if owner is not None:
key = station_key(owner[0], chainage, interval_m, owner[1])
elif index == 0:
key = station_key("bp", chainage, interval_m)
elif index == len(values) - 1:
key = station_key("ep", 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
+14 -9
View File
@@ -131,31 +131,36 @@ def _structure(sid, type_id, anchor, start=None, end=None):
def test_측점_열쇠_규칙():
stations = [{"chainage_m": s} for s in (0.0, 20.0, 40.0, 50.0, 60.0, 77.0, 80.0, 95.5)]
pipes = [(3, PipePoint(chainage_m=50.0))]
"""`common_util_station_key` 한 벌 — 관 · 구조물 id · id 없으면 at: · 같은 열쇠 둘째는 at:."""
stations = [{"chainage_m": s} for s in (0.0, 20.0, 33.3, 40.0, 50.0, 60.0, 77.0, 80.0, 95.5)]
with_id = PipePoint(chainage_m=50.0)
with_id.id = "p-7" # sub3 이 넣는 관 id
no_id = PipePoint(chainage_m=33.3)
structures = [_structure("w1", "retaining_wall", 77.0, 60.0, 80.0)]
keys = station_keys(stations, pipes, structures, 20.0)
keys = station_keys(stations, [with_id, no_id], structures, 20.0)
assert keys == {
"end:BP": 0.0,
"grid:1": 20.0,
"at:33.30": 33.3, # id 없는 관
"grid:2": 40.0,
"pipe:3": 50.0,
"structure:w1:start": 60.0,
"structure:w1": 77.0,
"structure:w1:end": 80.0,
"pipe:p-7": 50.0,
"structure:w1": 60.0, # 구간 시작 측점이 먼저 열쇠를 가짐
"at:77.00": 77.0, # 같은 구조물 열쇠 둘째 — at:
"at:80.00": 80.0,
"end:EP": 95.5,
}
assert "pipe:0" not in keys and "pipe:1" not in keys # 파일 차례 열쇠 없음
def test_이월은_열쇠로_누가거리가_밀려도():
old_keys = {
"end:BP": 0.0,
"grid:1": 20.0,
"pipe:0": 50.0,
"pipe:p-7": 50.0,
"structure:w1": 77.0,
"grid:5": 100.0,
}
new_keys = {"end:BP": 0.0, "grid:1": 20.0, "pipe:0": 57.0, "structure:w1": 84.0}
new_keys = {"end:BP": 0.0, "grid:1": 20.0, "pipe:p-7": 57.0, "structure:w1": 84.0}
designs = {
0.0: {"ground_type": "soil"},
20.0: {"ground_type": "rock", "berm": {"on": True}, "cut_area_m2": 9.0},