refactor(476-1 ②): 측점 · 1차 계획선 셈과 파일 쓰기 나눔(동작 같음)

- Engine_Sections `build_sections`(측점 틀 · 지반 · 438 1차 계획선 · 포장 제안 · 파일 안 씀) · run_section_generation = 읽기 → 셈 → 쓰기
- 횡단 이름 규칙 → 새 `B05_Profile_Engine_Sections_Names`(700줄 · 다시 내보냄)
- resolve_extra_stations 사본 칸(structures · plus) · 따라감 셈 몫 `followed_structures` · `followed_plus_rows` · `followed_pipes` · `section_seed` · `seat_base_pvi` 뗌
- 체인 · 추천 초기 종단이 같은 조각을 부를 자리

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 16:36:26 +09:00
co-authored by Claude Opus 5.5
parent 858ef3b40f
commit b0e38de44c
6 changed files with 230 additions and 142 deletions
+77 -104
View File
@@ -23,6 +23,17 @@ from B05_Profile.B05_Profile_Engine_Sections_Core import (
SectionGenerationOptions,
generate_sections,
)
from B05_Profile.B05_Profile_Engine_Sections_Names import ( # noqa: F401 — 부르는 곳이 여기서 씀
CROSS_KEY_PATTERN,
cross_filename,
cross_files_in_order,
cross_key,
cross_station,
find_cross_file,
parse_cross_key,
prune_stale_cross_files,
valid_cross_names,
)
from B05_Profile.B05_Profile_Structures_Repository import load_structures
from B05_Profile.B05_Profile_Structures_Schema import (
structure_type_map,
@@ -118,82 +129,6 @@ def _read_design_curve_table(project_root: Path) -> dict[str, Any]:
return data if isinstance(data, dict) else {}
# ── 횡단 이름 규칙(한 벌) — 파일 이름 · B07 도면 id · 찾기가 모두 이것을 씀 ────────────
#: 이름 속 측점 글 — 정수 m `00120` · 소수 측점 `00119.80`
CROSS_KEY_PATTERN = r"(\d+(?:\.\d{2})?)"
def cross_station(chainage_m: float) -> int | float:
"""측점 → 이름 열쇠 값 — 정수 m(0.005m 안)면 int(옛 이름 · 키 그대로) · 아니면 소수 2자리."""
value = round(float(chainage_m), 2)
whole = round(value)
return int(whole) if abs(value - whole) < 0.005 else value
def cross_key(chainage_m: float) -> str:
"""이름 속 측점 글 — `00120`(정수 · 옛 이름 그대로) · `00119.80`(소수 측점)."""
station = cross_station(chainage_m)
return f"{station:05d}" if isinstance(station, int) else f"{station:08.2f}"
def parse_cross_key(text: str) -> int | float:
"""`cross_key` 되읽기."""
return cross_station(float(text))
def cross_filename(chainage_m: float) -> str:
"""측점 chainage에 대응하는 횡단면 파일명(단일 규칙)."""
return f"cross_{cross_key(chainage_m)}m.json"
def find_cross_file(cross_dir: Path, chainage_m: float) -> Path:
"""읽기용 — 지금 이름이 없으면 옛 정수 m 이름(소수 측점이 정수로 붙던 옛 프로젝트)."""
path = cross_dir / cross_filename(chainage_m)
legacy = cross_dir / f"cross_{int(round(float(chainage_m))):05d}m.json"
return path if path.exists() or not legacy.exists() else legacy
def valid_cross_names(cross_dir: Path, stations: list[Any]) -> set[str]:
"""stations 에 맞는 cross_*.json 파일명 집합 — 읽기 전용(지우지 않음 · GET 이 씀).
측점 정보가 비면 빈 집합(호출부는 빈 집합이면 필터를 생략한다).
"""
# 옛 프로젝트는 소수 측점 파일이 정수 m 이름이라 찾기 규칙(find_cross_file)으로 가림
return {
find_cross_file(cross_dir, station["chainage_m"]).name
for station in stations
if isinstance(station, dict) and isinstance(station.get("chainage_m"), (int, float))
}
def cross_files_in_order(cross_dir: Path, stations: list[Any]) -> list[Path]:
"""잔재를 걸러 읽을 cross_*.json — **측점 차례**(이름 차례면 `01080.16` 이 `01080` 앞)."""
valid = valid_cross_names(cross_dir, stations)
def chainage(path: Path) -> tuple[float, str]:
match = re.fullmatch(rf"cross_{CROSS_KEY_PATTERN}m", path.stem)
return (float(match.group(1)) if match else float("inf"), path.name)
files = [path for path in cross_dir.glob("cross_*.json") if not valid or path.name in valid]
return sorted(files, key=chainage)
def prune_stale_cross_files(cross_dir: Path, stations: list[Any]) -> set[str]:
"""stations에 없는 잔재 cross_*.json을 삭제하고 유효 파일명 집합을 반환한다.
쓰는 쪽(측점 만들기 · 측점 목록 고침)만 부른다 — 조회(GET)는 `valid_cross_names`.
측점 정보가 비어 있으면 오삭제를 피하기 위해 아무것도 지우지 않고
빈 집합을 반환한다(호출부는 빈 집합이면 필터를 생략한다).
"""
valid = valid_cross_names(cross_dir, stations)
if not valid:
return valid
for path in cross_dir.glob("cross_*.json"):
if path.name not in valid:
path.unlink(missing_ok=True)
return valid
def _cross_summary(cross_section: dict[str, Any]) -> dict[str, Any]:
"""횡단면 상세에서 DB data 컬럼에 저장할 요약을 만든다."""
samples = cross_section.get("samples", [])
@@ -297,7 +232,12 @@ def relabel_pipe_stations(
def resolve_extra_stations(
project_root: Path, pipes: list[PipePoint], xy: Sequence[Sequence[float]] | None = None
project_root: Path,
pipes: list[PipePoint],
xy: Sequence[Sequence[float]] | None = None,
*,
structures: list[Any] | None = None,
plus: list[float] | None = None,
) -> tuple[tuple[float, str], ...]:
"""구조물(비정규) 측점의 **단일 공급자** — 관 정본 + 구조물 정본 A군에서 파생한다.
@@ -314,12 +254,14 @@ def resolve_extra_stations(
거울 시험 = `resources/tester/test_322_3_plan_mirror.py` · `test_272_1_station_list.py`.
추가 측점(368-2) = `plus_stations.json` · 이름 "".
`xy` = 지금 노선 — 노선 밖(시점 앞 · 종점 뒤 · `route_outside`) 관 · 구조물은 뺌(459).
`structures` · `plus` = 파일 대신 따라간 사본(476 추천 초기 종단 · 파일 안 씀).
"""
from B05_Profile.B05_Profile_Plus_Stations import plus_chainages
from common_util.common_util_route_follow import route_outside
try:
structures = load_structures(str(project_root))[1]
if structures is None:
structures = load_structures(str(project_root))[1]
except Exception:
logger.exception("B05: 구조물 정본을 읽지 못했습니다 (관 유래 측점만 씁니다)")
structures = []
@@ -332,7 +274,9 @@ def resolve_extra_stations(
]
return tuple(
(float(station["chainage_m"]), str(station["title"]))
for station in plan_stations(pipes, structures, plus_chainages(project_root))
for station in plan_stations(
pipes, structures, plus_chainages(project_root) if plus is None else plus
)
)
@@ -450,6 +394,55 @@ def _annotate_pavement_suggestions(
station["pavement_grade_limit_pct"] = round(unpaved_limit, 2)
def build_sections(
polyline: list[list[float]],
sampler: Any,
pipes: list[PipePoint],
extras: tuple[tuple[float, str], ...],
*,
options: SectionGenerationOptions | None,
grade_options: GradeDesignOptions | None,
source_snapshot: dict[str, Any],
crs: str | None,
design_curves: list[dict[str, Any]],
station_snap: bool,
station_snap_step: float | None,
) -> tuple[dict[str, Any], dict[str, Any] | None]:
"""측점 틀 · 지반 · 1차 계획선(438) · 포장 제안 — 파일 안 씀 · (결과, 계획선 요약).
종횡단 생성(`run_section_generation`)과 추천 후보 초기 종단(476 · `..._RouteInitial_Initial`)
이 같은 셈 한 벌."""
result = generate_sections(
polyline,
sampler,
replace(options or SectionGenerationOptions(), extra_stations=extras),
source_snapshot=source_snapshot,
crs=crs,
design_curves=design_curves,
station_snap=station_snap,
station_snap_step=station_snap_step,
)
grade_summary = _append_design_profiles(
result["longitudinal"],
grade_options,
(result.get("options") or {}).get("station_interval_m"),
# 1차 계획선의 변화점 = 배수유역도가 확정한 배관 배치 측점(측점 생성과 같은 목록).
pipe_chainages=[pipe.chainage_m for pipe in pipes],
# 시설 제원 최소 여유(관경+토피 등)로 계획선 들어올림 — 사용자 스위치(기본 해제 · 09-01).
pipe_clearances=(
dict(pipe_anchor_clearances(pipes))
if grade_options and grade_options.enforce_pipe_clearance
else None
),
)
# 계획선 경사 기반 포장 제안(법정 상한 초과 측점) — 비치명적.
try:
_annotate_pavement_suggestions(result["longitudinal"], grade_options)
except Exception:
logger.exception("B05 포장 제안 판정 실패 (종횡단은 유지)")
return result, grade_summary
def run_section_generation(
project_root: Path,
route_data_path: str,
@@ -487,10 +480,13 @@ def run_section_generation(
extras = ()
logger.info("B05 종횡단: 구조물 측점 %d건 파생(관 %d건)", len(extras), len(pipes))
result = generate_sections(
result, grade_summary = build_sections(
polyline,
sampler,
replace(options or SectionGenerationOptions(), extra_stations=extras),
pipes,
extras,
options=options,
grade_options=grade_options,
source_snapshot={"filter": filter_key, "method": method, "smooth": smooth},
crs=crs,
# 곡선부 확폭의 정본 — 설계 곡선표(시·종점·반지름)를 그대로 쓴다. 없으면 빈 목록이라
@@ -505,29 +501,6 @@ def run_section_generation(
cross_dir = stage_root / "cross_sections"
long_dir.mkdir(parents=True, exist_ok=True)
cross_dir.mkdir(parents=True, exist_ok=True)
# 종단면 저장 (계획선은 저장 직전에 종단 데이터에 붙인다)
grade_summary = _append_design_profiles(
result["longitudinal"],
grade_options,
(result.get("options") or {}).get("station_interval_m"),
# 1차 계획선의 변화점 = 배수유역도가 확정한 배관 배치 측점(측점 생성과 같은 목록).
pipe_chainages=[pipe.chainage_m for pipe in pipes],
# 시설 제원이 요구하는 최소 여유(관경+토피 등) — 계획선이 그만큼 들려야 관·구체가
# 들어갈 자리가 생긴다(2026-08-23 사용자 지시). 다만 이 들어올림이 배수 자리마다
# 계획고를 잡아당겨 제어가 어려워, 적용 여부를 사용자 스위치로 뺐다(2026-09-01
# 사용자 지시 — 기본 해제). 산식은 그대로이고 켜고 끄는 것만 바뀐다.
pipe_clearances=(
dict(pipe_anchor_clearances(pipes))
if grade_options and grade_options.enforce_pipe_clearance
else None
),
)
# 계획선 경사 기반 포장 제안(법정 상한 초과 측점) — 비치명적.
try:
_annotate_pavement_suggestions(result["longitudinal"], grade_options)
except Exception:
logger.exception("B05 포장 제안 판정 실패 (종횡단은 유지)")
long_file = long_dir / "longitudinal.json"
atomic_write_json(long_file, result["longitudinal"])
long_summary = {
@@ -0,0 +1,82 @@
"""횡단 이름 규칙 한 벌 — 파일 이름 · B07 도면 id · 찾기 · 잔재 거름.
`B05_Profile_Engine_Sections` 에서 나눔(700줄) · 그 모듈이 다시 내보냄."""
import re
from pathlib import Path
from typing import Any
# ── 횡단 이름 규칙(한 벌) — 파일 이름 · B07 도면 id · 찾기가 모두 이것을 씀 ────────────
#: 이름 속 측점 글 — 정수 m `00120` · 소수 측점 `00119.80`
CROSS_KEY_PATTERN = r"(\d+(?:\.\d{2})?)"
def cross_station(chainage_m: float) -> int | float:
"""측점 → 이름 열쇠 값 — 정수 m(0.005m 안)면 int(옛 이름 · 키 그대로) · 아니면 소수 2자리."""
value = round(float(chainage_m), 2)
whole = round(value)
return int(whole) if abs(value - whole) < 0.005 else value
def cross_key(chainage_m: float) -> str:
"""이름 속 측점 글 — `00120`(정수 · 옛 이름 그대로) · `00119.80`(소수 측점)."""
station = cross_station(chainage_m)
return f"{station:05d}" if isinstance(station, int) else f"{station:08.2f}"
def parse_cross_key(text: str) -> int | float:
"""`cross_key` 되읽기."""
return cross_station(float(text))
def cross_filename(chainage_m: float) -> str:
"""측점 chainage에 대응하는 횡단면 파일명(단일 규칙)."""
return f"cross_{cross_key(chainage_m)}m.json"
def find_cross_file(cross_dir: Path, chainage_m: float) -> Path:
"""읽기용 — 지금 이름이 없으면 옛 정수 m 이름(소수 측점이 정수로 붙던 옛 프로젝트)."""
path = cross_dir / cross_filename(chainage_m)
legacy = cross_dir / f"cross_{int(round(float(chainage_m))):05d}m.json"
return path if path.exists() or not legacy.exists() else legacy
def valid_cross_names(cross_dir: Path, stations: list[Any]) -> set[str]:
"""stations 에 맞는 cross_*.json 파일명 집합 — 읽기 전용(지우지 않음 · GET 이 씀).
측점 정보가 비면 빈 집합(호출부는 빈 집합이면 필터를 생략한다).
"""
# 옛 프로젝트는 소수 측점 파일이 정수 m 이름이라 찾기 규칙(find_cross_file)으로 가림
return {
find_cross_file(cross_dir, station["chainage_m"]).name
for station in stations
if isinstance(station, dict) and isinstance(station.get("chainage_m"), (int, float))
}
def cross_files_in_order(cross_dir: Path, stations: list[Any]) -> list[Path]:
"""잔재를 걸러 읽을 cross_*.json — **측점 차례**(이름 차례면 `01080.16` 이 `01080` 앞)."""
valid = valid_cross_names(cross_dir, stations)
def chainage(path: Path) -> tuple[float, str]:
match = re.fullmatch(rf"cross_{CROSS_KEY_PATTERN}m", path.stem)
return (float(match.group(1)) if match else float("inf"), path.name)
files = [path for path in cross_dir.glob("cross_*.json") if not valid or path.name in valid]
return sorted(files, key=chainage)
def prune_stale_cross_files(cross_dir: Path, stations: list[Any]) -> set[str]:
"""stations에 없는 잔재 cross_*.json을 삭제하고 유효 파일명 집합을 반환한다.
쓰는 쪽(측점 만들기 · 측점 목록 고침)만 부른다 — 조회(GET)는 `valid_cross_names`.
측점 정보가 비어 있으면 오삭제를 피하기 위해 아무것도 지우지 않고
빈 집합을 반환한다(호출부는 빈 집합이면 필터를 생략한다).
"""
valid = valid_cross_names(cross_dir, stations)
if not valid:
return valid
for path in cross_dir.glob("cross_*.json"):
if path.name not in valid:
path.unlink(missing_ok=True)
return valid
+9 -4
View File
@@ -75,6 +75,14 @@ def follow_plus_stations(project_root: str | Path, new_xy: Sequence[Sequence[flo
rows = load_plus_stations(project_root)
if not rows:
return 0
followed = followed_plus_rows(rows, new_xy)
if followed["moved"]:
atomic_write_json(str(plus_stations_path(project_root)), {"stations": followed["rows"]})
return int(followed["moved"])
def followed_plus_rows(rows: list[dict[str, Any]], new_xy: Sequence[Sequence[float]]) -> dict:
"""추가 측점을 새 노선에 투영(저장 안 함) → `{rows, moved}` — 체인 · 추천 초기 종단(476)."""
results = run_calc(
["facility.plus_follow"],
{
@@ -89,7 +97,4 @@ def follow_plus_stations(project_root: str | Path, new_xy: Sequence[Sequence[flo
)
if results is None:
raise RuntimeError("추가 측점 따라감 실패 — 계산 Node 입구(facility.plus_follow)")
followed = results["facility.plus_follow"]
if followed["moved"]:
atomic_write_json(str(plus_stations_path(project_root)), {"stations": followed["rows"]})
return int(followed["moved"])
return results["facility.plus_follow"]
+31 -20
View File
@@ -22,7 +22,7 @@ API — `POST /api/projects/{id}/route/replan`(`B05_Profile_Router_Replan.replan
(`B05_Profile_Route_Save_Parts`) · 배수유역 저장분은 노선 지문이 달라 「낡음」(B04 `_resolve`).
- 계획고 = 바뀐 자리만 새 지반 첫 계획선 · 같은 자리(옆 1 m 안) 사용자 계획고 지킴
(`B05_Profile_Engine_Grade_Carry` · 옛 · 새 **계획 폴리라인** — 화면 planline 과 같은 선) ·
추천 [적용](`as_initial`) = 옮기지 않음 — 균형 종단(`apply_profile_target`) 또는 1차 계획선(438).
추천 [적용](`as_initial`) = 옮기지 않음 — 균형 종단(`apply_profile_target`) · 1차 계획선(438).
- 초기값은 여기서 안 뜸 — 부르는 쪽이 `as_initial` 일 때만(자동설계 체인 직후 + 추천 [적용]).
"""
@@ -165,6 +165,31 @@ def _xy(project_root: Path, route_data_path: str | None) -> list[list[float]]:
return []
def followed_pipes(
old_pipes: list, old_xy: list[list[float]], points: list[dict[str, float]], streams: list
) -> tuple[list, int, list]:
"""관 따라감 + 새 세류선 교차점 기본 관(저장 안 함) → (짝, 교차 수, 새 노선 정점) — 체인 ·
추천 초기 종단(476) 한 벌."""
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import _base_pipes
from common_util.common_util_drainage_pipes import fill_pipe_coordinates
vertices = vertices_of([(p["x"], p["y"]) for p in points])
fill_pipe_coordinates(old_pipes, vertices_of(old_xy))
pairs = follow_pipes(old_pipes, vertices)
total = float(vertices[-1].chainage_m)
crossing = add_crossing_pipes(pairs, _base_pipes(vertices, streams), old_xy, total)
return pairs, crossing, vertices
def section_seed(request: Any, old_section_options: dict[str, Any] | None) -> dict[str, Any]:
"""체인 종횡단 생성 옵션(건너뛴 첫 생성이 남겼을 값) — 체인 · 추천 초기 종단(476) 한 벌."""
from B05_Profile.B05_Profile_Router import _section_options
seed = asdict(_section_options(request, old_section_options))
seed.pop("extra_stations", None)
return seed
async def _place_pipes(
project_id: UUID,
stored_path: str,
@@ -178,22 +203,13 @@ async def _place_pipes(
노선은 범위를 안 잘라 누가거리가 갈림 · 세류선만 거기서. 좌표 없는 옛 관은 옛 노선
자리로 먼저 채움(459).
"""
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import _base_pipes
from B04_PreProcess.B04_PreProcess_Router_Watershed import _prepare
from common_util.common_util_drainage_pipes import (
fill_pipe_coordinates,
route_signature,
save_pipe_points,
)
from common_util.common_util_drainage_pipes import route_signature, save_pipe_points
from common_util.common_util_route_follow import route_outside
prepared = await _prepare(project_id)
streams = [] if isinstance(prepared, JSONResponse) else prepared["streams"] # 도엽 없음
vertices = vertices_of([(p["x"], p["y"]) for p in points])
fill_pipe_coordinates(old_pipes, vertices_of(old_xy))
pairs = follow_pipes(old_pipes, vertices)
total = float(vertices[-1].chainage_m)
crossing = add_crossing_pipes(pairs, _base_pipes(vertices, streams), old_xy, total)
pairs, crossing, vertices = followed_pipes(old_pipes, old_xy, points, streams)
await asyncio.to_thread(
save_pipe_points,
stored_path,
@@ -259,7 +275,7 @@ async def _carry_profile(
def carries_cross_inputs(initial: bool) -> bool:
"""횡단 손 입력을 새 측점(같은 자리)에 넘기나 — 추천 [적용]도 넘김(475-3 · 사용자 답 대기 476)."""
"""횡단 손 입력을 새 측점(같은 자리)에 넘기나 — [적용]도 넘김(475-3 · 사용자 답 대기 476)."""
return True
@@ -376,11 +392,7 @@ async def run_route_save_chain(
from B03_FileInput.B03_FileInput_Service_Chain import _log_steps, redesign_solve_request
from B05_Profile.B05_Profile_Engine_Sections import _STAGE_SUBDIR
from B05_Profile.B05_Profile_Plus_Stations import follow_plus_stations, plus_stations_path
from B05_Profile.B05_Profile_Router import (
_section_options,
confirm_latest_route,
solve_route_record,
)
from B05_Profile.B05_Profile_Router import confirm_latest_route, solve_route_record
from B05_Profile.B05_Profile_Structures_Repository import load_structures, structures_file_path
from B05_Profile.B05_Profile_Structures_Router import follow_structures_to_route
from B06_Section.B06_Section_Repository import (
@@ -456,8 +468,7 @@ async def run_route_save_chain(
pairs, pipes = await _place_pipes(project_id, stored_path, old_pipes, old_xy, points)
marks.append(("관 따라감 · 교차점", time.perf_counter()))
seed = asdict(_section_options(request, old_section_options))
seed.pop("extra_stations", None)
seed = section_seed(request, old_section_options)
made = await regenerate_route_sections(
project_id, new_route_id, float(seed["cross_half_width_m"]), stored_options=seed
)
+10 -4
View File
@@ -355,15 +355,22 @@ async def refresh_expected_route(project_id: UUID) -> None:
def _set_base_pvi(path: Path, stations: list[float], plan: list[float]) -> int:
"""종단 정본의 1차 변화점(`base_pvi`) 계획고를 `stations` · `plan` 직선 보간으로 바꿔 쓴다."""
import json
from common_util.common_util_json import atomic_write_json
longitudinal = json.loads(path.read_text(encoding="utf-8"))
count = seat_base_pvi(longitudinal, stations, plan)
atomic_write_json(path, longitudinal)
return count
def seat_base_pvi(longitudinal: dict[str, Any], stations: list[float], plan: list[float]) -> int:
"""1차 변화점 계획고를 균형 종단에 얹음(제자리 · 파일 안 씀) — [적용] 체인 · 추천 초기 종단(476)
한 벌."""
import numpy as np
from B05_Profile.B05_Profile_Engine_Grade_Alignment import AlignmentPolicy
from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import held_pass_through
from common_util.common_util_json import atomic_write_json
longitudinal = json.loads(path.read_text(encoding="utf-8"))
alignment = longitudinal["profile_alignment"]
base = alignment["base_pvi"]
base_s = np.array([float(item["chainage_m"]) for item in base])
@@ -374,7 +381,6 @@ def _set_base_pvi(path: Path, stations: list[float], plan: list[float]) -> int:
)
for item, value in zip(base, base_z, strict=True):
item["elevation_m"] = round(float(value), 6) # 직선 판정(426) 1e-5 m 안
atomic_write_json(path, longitudinal)
return len(base)
+21 -10
View File
@@ -121,6 +121,26 @@ async def _route_xy(project_id: UUID, root: str) -> list[list[float]] | None:
return None
def followed_structures(
project_root: Path,
items: list[Any],
old_route_data_path: str | None,
new_xy: list[dict[str, float]],
) -> list[Any]:
"""구조물을 새 노선 땅 위 자리로 옮김(제자리 · 저장 안 함) — 노선 저장 체인 · 추천 초기
종단(476) 한 벌. 좌표 없는 옛 자료는 옛 노선으로 먼저 채움."""
from B05_Profile.B05_Profile_Engine_Sections import _load_route_polyline
if old_route_data_path:
try:
fill_structure_coordinates(
items, _load_route_polyline(project_root, old_route_data_path), only_missing=True
)
except (OSError, ValueError):
logger.warning("B05 구조물 따라감: 옛 노선을 못 읽어 좌표 없는 구조물은 그대로")
return follow_structures(items, [(point["x"], point["y"]) for point in new_xy])
def follow_structures_to_route(
project_root: Path, old_route_data_path: str | None, new_xy: list[dict[str, float]]
) -> dict[str, int]:
@@ -129,21 +149,12 @@ def follow_structures_to_route(
좌표가 없는 옛 자료는 옛 노선(아직 안 바뀐 경로 파일)으로 먼저 채운다. 실패는 비치명 —
구조물은 옛 누가거리 그대로 남는다(지금까지와 같음).
"""
from B05_Profile.B05_Profile_Engine_Sections import _load_route_polyline
root = str(project_root)
revision, items = load_structures(root)
if not items:
return {"moved": 0, "far": 0}
if old_route_data_path:
try:
fill_structure_coordinates(
items, _load_route_polyline(project_root, old_route_data_path), only_missing=True
)
except (OSError, ValueError):
logger.warning("B05 구조물 따라감: 옛 노선을 못 읽어 좌표 없는 구조물은 그대로")
before = [(item.chainage_m, item.start_m, item.end_m) for item in items]
result = follow_structures(items, [(point["x"], point["y"]) for point in new_xy])
result = followed_structures(project_root, items, old_route_data_path, new_xy)
moved = sum(
before[index] != (item.chainage_m, item.start_m, item.end_m)
for index, item in enumerate(items)