- 첫 계획선(계획고 = 지반고) base_pvi TS 짝 = B05_Profile/logic/B05_Profile_Grade_Ground.ts(1순위만 · 못 서는 땅은 null = 서버로 다시 셈) - 관 자리 투영 TS 짝 · 구조물 따라감 py · ts 짝 = common_util_route_follow(정수 m · 구간 끝 밂 · 0 ~ 연장 자름 · 20 m 넘으면 멀어짐 표시 · 상수 STRUCTURE_ROUTE_DRIFT_MAX_M) - 구조물 x · y 저장(저장 때 노선 위 자리 · 자리가 그대로면 옛 좌표 지킴 · 옛 자료는 읽을 때 채움) · 노선 [확인] 때 구조물 정본이 좌표를 따라감 - 노선 바뀜 때 계획고 옮김 = 바뀐 구간만 새 지반 첫 계획선 · 앞 그대로 · 뒤 길이 차만큼 밂(B05_Profile_Engine_Grade_Carry.py ↔ logic/B05_Profile_Grade_Carry.ts) - 거울 시험 test_344_2_route_pairs(+ helper cjs) · buildCurves · evaluateAt 내보냄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
128 lines
5.3 KiB
Python
128 lines
5.3 KiB
Python
"""노선이 바뀔 때 계획고 옮김 — 바뀐 구간만 지반에 다시 맞추고 그 밖 편집은 길이 차만큼 민다(344-2).
|
||
|
||
사용자 확정(10-16 · 341 결정 1): 노선 끝쪽을 고쳤는데 앞쪽 계획고가 날아가지 않게 한다.
|
||
|
||
- 바뀐 구간 = 옛 · 새 노선 정점열의 같은 앞머리 · 같은 꼬리를 뺀 가운데(`changed_span`) — 바뀐
|
||
노드 앞 · 뒤 접선점 사이. 원호 정점까지 같은지 보므로 곡선이 바뀐 이웃 노드 범위도 들어간다.
|
||
- 구간 앞 = 같은 땅 · 같은 누가거리 → 그대로. 구간 뒤 = 같은 땅 · 누가거리만 길이 차만큼 밀림.
|
||
- 구간 안 = 새 지반 첫 계획선(`design_ground_following_profile` 의 base_pvi) 꼭짓점으로
|
||
갈아 끼움 · 그 안 편집(계획고 · 종단곡선 R)은 버림.
|
||
- 옮기는 것 = base_pvi · station_offsets · curve_radii(누가거리 글자 키).
|
||
|
||
짝 = TS `B05_Profile/logic/B05_Profile_Grade_Carry.ts`(같은 규칙) · 거울 시험 =
|
||
`resources/tester/test_344_2_route_pairs.py`. 서버 정본은 이쪽(346-3 [저장]이 부름).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from typing import Any
|
||
|
||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import chainage_key
|
||
|
||
# 같은 정점으로 볼 거리(m) — 저장 좌표 0.0001 m 자리 · 원호 다시 뽑기 잡음보다 크게.
|
||
SAME_VERTEX_M = 1e-3
|
||
_EPS = 1e-6
|
||
|
||
|
||
def _cumulative(xy: list[tuple[float, float]]) -> list[float]:
|
||
total = [0.0]
|
||
for (x0, y0), (x1, y1) in zip(xy, xy[1:]):
|
||
total.append(total[-1] + math.hypot(x1 - x0, y1 - y0))
|
||
return total
|
||
|
||
|
||
def _same(a: tuple[float, float], b: tuple[float, float]) -> bool:
|
||
return abs(a[0] - b[0]) <= SAME_VERTEX_M and abs(a[1] - b[1]) <= SAME_VERTEX_M
|
||
|
||
|
||
def changed_span(
|
||
old_xy: list[tuple[float, float]], new_xy: list[tuple[float, float]]
|
||
) -> dict[str, Any] | None:
|
||
"""옛 · 새 노선 정점열 → 바뀐 누가거리 구간. 같으면 None.
|
||
|
||
`start_m` = 같은 앞머리 끝(옛 = 새) · `old_end_m` · `new_end_m` = 같은 꼬리 시작 ·
|
||
`delta_m` = 새 연장 − 옛 연장 · `start_fixed` · `end_fixed` = 앞머리 · 꼬리가 한 점이라도
|
||
같은가.
|
||
"""
|
||
old_xy = [(float(x), float(y)) for x, y in old_xy]
|
||
new_xy = [(float(x), float(y)) for x, y in new_xy]
|
||
limit = min(len(old_xy), len(new_xy))
|
||
prefix = 0
|
||
while prefix < limit and _same(old_xy[prefix], new_xy[prefix]):
|
||
prefix += 1
|
||
if prefix == len(old_xy) == len(new_xy):
|
||
return None
|
||
suffix = 0
|
||
while suffix < limit - prefix and _same(old_xy[-1 - suffix], new_xy[-1 - suffix]):
|
||
suffix += 1
|
||
old_cum, new_cum = _cumulative(old_xy), _cumulative(new_xy)
|
||
return {
|
||
"start_m": old_cum[prefix - 1] if prefix else 0.0,
|
||
"old_end_m": old_cum[len(old_xy) - suffix] if suffix else old_cum[-1],
|
||
"new_end_m": new_cum[len(new_xy) - suffix] if suffix else new_cum[-1],
|
||
"delta_m": new_cum[-1] - old_cum[-1],
|
||
"start_fixed": prefix > 0,
|
||
"end_fixed": suffix > 0,
|
||
}
|
||
|
||
|
||
def _before(span: dict[str, Any], s: float) -> bool:
|
||
"""구간 앞(그대로 둠) — 앞머리가 같으면 그 끝점까지."""
|
||
start = float(span["start_m"])
|
||
return s < start - _EPS or (bool(span["start_fixed"]) and s <= start + _EPS)
|
||
|
||
|
||
def _after(span: dict[str, Any], s: float, end: float) -> bool:
|
||
"""구간 뒤(밂) — 꼬리가 같으면 그 시작점부터."""
|
||
return s > end + _EPS or (bool(span["end_fixed"]) and s >= end - _EPS)
|
||
|
||
|
||
def carry_profile_edits(
|
||
base_pvi: list[dict[str, float]],
|
||
edits: dict[str, Any] | None,
|
||
span: dict[str, Any],
|
||
ground_pvi: list[dict[str, float]],
|
||
) -> tuple[list[dict[str, float]], dict[str, dict[str, float]]]:
|
||
"""옛 꼭짓점 · 편집을 새 노선으로 옮긴다 → (새 base_pvi, 새 edits).
|
||
|
||
`ground_pvi` = 새 노선 지반 첫 계획선 꼭짓점(전 구간 · `design_ground_following_profile`) —
|
||
바뀐 구간 안 몫만 쓴다.
|
||
"""
|
||
delta = float(span["delta_m"])
|
||
old_end, new_end = float(span["old_end_m"]), float(span["new_end_m"])
|
||
|
||
def moved(s: float) -> float | None:
|
||
if _before(span, s):
|
||
return s
|
||
if _after(span, s, old_end):
|
||
return s + delta
|
||
return None # 바뀐 구간 안 — 버림
|
||
|
||
nodes: dict[float, dict[str, float]] = {}
|
||
for node in base_pvi:
|
||
at = moved(float(node["chainage_m"]))
|
||
if at is not None:
|
||
key = round(at, 3)
|
||
nodes[key] = {"chainage_m": round(at, 6), "elevation_m": float(node["elevation_m"])}
|
||
for node in ground_pvi:
|
||
s = float(node["chainage_m"])
|
||
if _before(span, s) or _after(span, s, new_end):
|
||
continue
|
||
nodes.setdefault(
|
||
round(s, 3), {"chainage_m": round(s, 6), "elevation_m": float(node["elevation_m"])}
|
||
)
|
||
|
||
carried: dict[str, dict[str, float]] = {}
|
||
for name in ("station_offsets", "curve_radii"):
|
||
values: dict[str, float] = {}
|
||
for key, value in ((edits or {}).get(name) or {}).items():
|
||
try:
|
||
at = moved(float(key))
|
||
except (TypeError, ValueError):
|
||
continue
|
||
if at is not None and value is not None:
|
||
values[chainage_key(at)] = float(value)
|
||
carried[name] = values
|
||
return [nodes[key] for key in sorted(nodes)], carried
|