- carry_base(파이썬) ↔ carryBase(TS) 짝 — 같은 자리 = 맞춘 옛 누가거리 · 바뀐 자리 = 앞뒤 같은 자리(없으면 시점 0 · 종점 새 연장 ↔ 옛 연장) 사이 새 누가거리 비례로 옛 누가거리 · 높이 = 옛 기준 꼭짓점 · 지반 첫 계획선은 측점 자리만 - 덧 흠 — 손 [저장](as_initial 아님)은 initial_choice with_profile 을 적힌 값 그대로 - 낡은 시험 고침: test_400 · test_344_2 「바뀐 자리 = 지반」 · 「끝 꼭짓점 = 새 지반」 → 옛 종단 비례 · 옛 끝 높이 / test_459 비균형 「옮긴 시설 측점 = 새 원지반」 단언 지움(옛 규칙 · 시설 세움은 옮긴 계획선 높이) · 거울 단언은 그대로 - 새 시험: test_400 바뀐 자리 비례 4 갈래 · test_346_3 손 [저장] 종단 반영 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
169 lines
7.6 KiB
Python
169 lines
7.6 KiB
Python
"""노선이 바뀔 때 계획고 옮김 — 같은 자리는 옛 계획고 · 바뀐 자리는 옛 종단 길이 비례
|
||
(344-2 · 400 · 575 B).
|
||
|
||
사용자 확정(10-16 · 341 결정 1): 노선 끝쪽을 고쳤는데 앞쪽 계획고가 날아가지 않게 한다.
|
||
요구 250-2둘째(400): 평면을 고쳐도 크게 안 바뀐 자리의 종단 편집(직선화 · 자유지정 · 쉬프트 ·
|
||
▲▼)은 이어받음.
|
||
|
||
- `changed_span` = 옛 · 새 노선 정점열의 같은 앞머리 · 같은 꼬리를 뺀 가운데 —
|
||
「노선 그대로?」 · 범위.
|
||
- 같은 자리(`line_matcher`) = 새 선 점을 옛 선에 투영 · 옆 `SAME_LINE_TOL_M` 안 · 방향 30° 안
|
||
(측점 스냅이 뒤 교점을 밀어도 이어받음).
|
||
- 자동 꼭짓점(`carry_base`) · 편집(Node `carry_seat`)은 **새 측점 위에** 다시 놓음 — 같은 자리면
|
||
옛 선 그 자리 높이 · 아니면 앞뒤 같은 자리 사이 길이 비례 자리 옛 높이 · 편집은 옛 사용자
|
||
꼭짓점이 닿은 자리만 ·
|
||
R 은 가까운 측점. 편집 셈 = TS `carryEdits`(서버 Node · 511-B B-3) — 파이썬 짝 없음.
|
||
|
||
짝 = TS `B05_Profile/logic/B05_Profile_Grade_Carry.ts`(같은 규칙) · 거울 시험 =
|
||
`resources/tester/test_400_profile_carry.py` · `test_344_2_route_pairs.py`.
|
||
서버 정본은 이쪽(346-3 [저장]이 부름).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from collections.abc import Callable
|
||
from typing import Any
|
||
|
||
import numpy as np
|
||
|
||
# 같은 자리로 볼 옆 어긋남(m) — 브레인 정함 10-17 · 바꿀 수 있음 · TS `SAME_LINE_TOL_M` 짝.
|
||
SAME_LINE_TOL_M = 1.0
|
||
# 같은 자리로 볼 방향 차(cos 30°) — 엇갈려 지나는 선 · 머리핀 맞은편을 뺌.
|
||
_SAME_DIRECTION_COS = 0.8660254037844386
|
||
# 같은 정점으로 볼 거리(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 _segments(xy: list[tuple[float, float]]) -> np.ndarray:
|
||
"""토막 표 [x, y, ux, uy, 길이, 시작 누가거리] — 길이 0 토막은 뺌(누가거리는 셈)."""
|
||
rows: list[tuple[float, ...]] = []
|
||
start = 0.0
|
||
for (x0, y0), (x1, y1) in zip(xy, xy[1:]):
|
||
dx, dy = float(x1) - float(x0), float(y1) - float(y0)
|
||
length = math.sqrt(dx * dx + dy * dy)
|
||
if length > _EPS:
|
||
rows.append((float(x0), float(y0), dx / length, dy / length, length, start))
|
||
start += length
|
||
return np.array(rows, dtype=np.float64).reshape(-1, 6)
|
||
|
||
|
||
def line_matcher(
|
||
old_xy: list[tuple[float, float]],
|
||
new_xy: list[tuple[float, float]],
|
||
tol_m: float = SAME_LINE_TOL_M,
|
||
) -> Callable[[float], float | None]:
|
||
"""새 누가거리 → 같은 자리 옛 누가거리(옆 `tol_m` · 방향 30° 안) · 아니면 None."""
|
||
olds, news = _segments(old_xy), _segments(new_xy)
|
||
|
||
def match(chainage: float) -> float | None:
|
||
if not len(olds) or not len(news):
|
||
return None
|
||
ahead = np.nonzero(chainage <= news[:, 5] + news[:, 4])[0]
|
||
x, y, ux, uy, length, start = news[ahead[0] if len(ahead) else -1]
|
||
along = min(max(chainage - start, 0.0), length)
|
||
qx, qy = x + ux * along, y + uy * along
|
||
facing = olds[olds[:, 2] * ux + olds[:, 3] * uy >= _SAME_DIRECTION_COS]
|
||
if not len(facing):
|
||
return None
|
||
ox, oy, oux, ouy, olen, ostart = facing.T
|
||
p = np.minimum(np.maximum((qx - ox) * oux + (qy - oy) * ouy, 0.0), olen)
|
||
dx, dy = qx - (ox + oux * p), qy - (oy + ouy * p)
|
||
gap = np.sqrt(dx * dx + dy * dy)
|
||
best = int(np.argmin(gap))
|
||
return float(ostart[best] + p[best]) if gap[best] <= tol_m else None
|
||
|
||
return match
|
||
|
||
|
||
def carry_base(
|
||
old_xy: list[tuple[float, float]],
|
||
new_xy: list[tuple[float, float]],
|
||
old_base: list[dict[str, float]],
|
||
ground_pvi: list[dict[str, float]],
|
||
tol_m: float = SAME_LINE_TOL_M,
|
||
) -> list[dict[str, float]]:
|
||
"""새 자동 꼭짓점 — 지반 첫 계획선 측점 자리마다 옛 자동선 높이(575 B).
|
||
|
||
같은 자리 = 맞춘 옛 누가거리 · 바뀐 자리 = 앞뒤 같은 자리(없으면 노선 시점 · 종점) 사이를 새
|
||
누가거리 비례로 옛 누가거리에 옮김(옛 종단 모양을 늘이고 줄임 · 다시 풀지 않음).
|
||
"""
|
||
if not old_base or not ground_pvi:
|
||
return [dict(node) for node in old_base]
|
||
match = line_matcher(old_xy, new_xy, tol_m)
|
||
base_s = np.array([float(n["chainage_m"]) for n in old_base], dtype=np.float64)
|
||
base_z = np.array([float(n["elevation_m"]) for n in old_base], dtype=np.float64)
|
||
chainages = [float(node["chainage_m"]) for node in ground_pvi]
|
||
hits = [match(t) for t in chainages]
|
||
# 비례 기준점 — 시점(0 · 0) · 같은 자리 · 종점(새 · 옛 연장) 중 두 누가거리 모두 늘어나는 것만
|
||
# ponytail: 앞에서부터 고름 — 같은 방향 먼 옛 토막에 잘못 맞은 점이 있으면 뒤 기준점을 버림
|
||
at, to = [0.0], [0.0]
|
||
ends = (_cumulative(list(new_xy))[-1], _cumulative(list(old_xy))[-1])
|
||
for t, s in [*zip(chainages, hits), ends]:
|
||
if s is not None and t > at[-1] and s > to[-1]:
|
||
at.append(t)
|
||
to.append(s)
|
||
out = []
|
||
for t, s in zip(chainages, hits):
|
||
s = float(np.interp(t, at, to)) if s is None else s
|
||
out.append({"chainage_m": t, "elevation_m": round(float(np.interp(s, base_s, base_z)), 6)})
|
||
return out
|
||
|
||
|
||
def carry_policy(
|
||
old_xy: list[tuple[float, float]],
|
||
new_xy: list[tuple[float, float]],
|
||
old_alignment: dict[str, Any],
|
||
new_alignment: dict[str, Any],
|
||
) -> dict[str, Any]:
|
||
"""노선 [저장] 정책(432) — 새 정책에 옛 기본 L · 옛 정책 없으면 새 정책. 바뀐 자리 새 꼭짓점은
|
||
새 지반 첫 계획선 측점 자리에 옛 높이 비례(`carry_base` · 575 B)."""
|
||
new_policy = dict(new_alignment.get("policy") or {})
|
||
old_policy = old_alignment.get("policy") or {}
|
||
if not old_policy:
|
||
return new_policy
|
||
return {**new_policy, "default_curve_length_m": old_policy.get("default_curve_length_m")}
|