457 lines
21 KiB
Python
457 lines
21 KiB
Python
"""예상노선(점 묶음)을 **계획노선 폴리라인**으로 바꾸는 자리.
|
|
|
|
왜 필요한가(2026-09-06 사용자 지시) — 예상노선은 폴리라인이 아니라 **점(포인트)으로 이뤄진
|
|
데이터**이고 규칙 없는 폴리라인과도 맞지 않는다. 그래서 계획노선은 **원데이터를 복사한 뒤
|
|
폴리라인으로 바꾼 것**이어야 하고, 그것이 **불변의 초기 데이터**가 된다. 유토곡선·3D 에
|
|
투영되는 선도, 사용자가 노드를 잡아 고치는 대상도 이 폴리라인이다.
|
|
|
|
**곡선 기준은 지식DB 값**(`resources/knowledge/technical_info/01_임도/02_상세설계/평면선형.md`,
|
|
근거는 산림자원법 시행규칙 별표2 Ⅰ.2.다) — 코드에서는 `config_system_design` 이 그대로 들고 있다.
|
|
· 최소곡선반지름 — 설계속도 40: 일반 60 / 특수 40 · 30: 30 / 20 · 20: 15 / 12 (중심선 기준)
|
|
· 배향곡선 중심선 반지름 10m 이상
|
|
· **내각 155° 이상**(교각 25° 이하)이면 곡선을 두지 않을 수 있음
|
|
|
|
**하는 일은 「모양 정리」뿐이다** — 점을 옮기지 않는다. 꺾이는 점(IP)을 그대로 두고 그 자리에
|
|
원호를 끼워 넣어 매끄럽게 잇는다. 원호가 들어갈 자리(접선 길이)가 모자라면 반지름을 줄여
|
|
맞추고, 법정 하한 아래로 내려가면 **줄이되 위반으로 표시**한다 — 자동으로 점을 옮겨
|
|
「고쳐 주지」 않는다(2026-09-06 사용자 확정: 자동 보정·차단은 하지 않고 경고만).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from common_util.common_util_route_polyline_geom import ( # noqa: F401 — 바탕은 나눈 파일 · 이름은 여기서 그대로
|
|
DUPLICATE_TOLERANCE_M,
|
|
SIMPLIFY_TOLERANCE_M,
|
|
MAX_NODE_SPACING_M,
|
|
MAX_REFINE_DEPTH,
|
|
ARC_STEP_DEG,
|
|
_distance,
|
|
dedupe_points,
|
|
_perpendicular_distance,
|
|
_douglas_peucker,
|
|
simplify_to_nodes,
|
|
_inner_angle_deg,
|
|
_unit,
|
|
_node_indices,
|
|
_point_to_segment_m,
|
|
_corner_deviation_m,
|
|
_arc_geometry,
|
|
_fit_radius_m,
|
|
_arc_points,
|
|
_turn_sign,
|
|
_line_intersection,
|
|
)
|
|
|
|
# 별표2 가 「곡선을 두지 않을 수 있다」고 하는 내각(도) — **지금은 쓰지 않는다.**
|
|
#
|
|
# 왜 안 쓰나(2026-09-07 사용자 지시) — 「내각 155 이상이면 곡선 생략은 반영하지 말자.
|
|
# 사용자가 계획 평면 노선을 수정할 때 문제가 될 것 같아.」 곡선이 있다 없다 하면 잡는
|
|
# 손잡이도 있다 없다 하고, 조금 폈다는 이유로 곡선이 사라지면 되돌릴 길이 없다. 그래서
|
|
# **꺾이는 자리에는 늘 곡선을 둔다.** 값은 법정 근거를 잃지 않게 남겨만 둔다.
|
|
STRAIGHT_INNER_ANGLE_DEG = 155.0
|
|
|
|
# 한 곡선으로 **묶을지** 볼 때 쓰는 문턱(도) — 위 155° 와 쓰임이 다르다.
|
|
#
|
|
# 왜 나눴나(2026-09-07 실측) — 155° 를 묶기 판정에까지 쓰면 **완만한 곡선이 쪼개진다**.
|
|
# 반지름 40m 짜리 원호를 점으로 흉내 내 넣으니 꺾임점 셋(내각 156°·138°·154°)이 나왔는데
|
|
# 양 끝 둘이 155° 위라 「직선」으로 갈려 가운데 하나만 곡선이 됐고, 그 짧은 현 사이에서는
|
|
# 반지름을 키울수록 원본에서 멀어졌다(평균 1.75 → 2.19m). 별표2 의 155° 는 **그 곡선의
|
|
# 교각**에 대한 규칙이지, 점군을 쪼갠 조각 하나하나에 대한 규칙이 아니다.
|
|
# 그래서 묶을 때는 「조금이라도 같은 쪽으로 돈다」로 모으고, 155° 는 **묶은 뒤 전체 교각**에
|
|
# 적용한다.
|
|
CURVE_GROUP_INNER_ANGLE_DEG = 179.0
|
|
|
|
# 여러 꺾임을 한 곡선으로 묶을 때 **허용할 최대 벗어남**(m). 평균이 나아져도 한 자리가
|
|
# 이보다 크게 벌어지면 안 묶고 낱개로 둔다. 꺾임점 뽑기 허용오차(4m)의 두 배 — 노드 자체가
|
|
# 원본에서 4m 안에 있으므로, 곡선이 그보다 크게 벗어나면 다른 모양이 된 것이다.
|
|
MERGE_MAX_GAP_M = 8.0
|
|
|
|
|
|
@dataclass
|
|
class RouteNode:
|
|
"""사용자가 잡아 옮기는 제어점 하나 = 원본 꺾임점(IP)."""
|
|
|
|
x: float
|
|
y: float
|
|
inner_angle_deg: float | None = None
|
|
"""직전·직후 구간이 이루는 내각(도). 끝점은 None."""
|
|
radius_m: float | None = None
|
|
"""이 자리에 끼운 원호 반지름(m). 곡선을 안 둔 자리는 None."""
|
|
tangent_m: float | None = None
|
|
"""접선 길이(m) = R·tan(교각/2). 곡선을 안 둔 자리는 None."""
|
|
violations: list[str] = field(default_factory=list)
|
|
"""법정 기준 위반 표시 — 값은 넣되 막지 않는다."""
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"x": round(self.x, 4),
|
|
"y": round(self.y, 4),
|
|
"inner_angle_deg": None
|
|
if self.inner_angle_deg is None
|
|
else round(self.inner_angle_deg, 2),
|
|
"radius_m": None if self.radius_m is None else round(self.radius_m, 2),
|
|
"tangent_m": None if self.tangent_m is None else round(self.tangent_m, 2),
|
|
"violations": list(self.violations),
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class RouteCurve:
|
|
"""계획노선의 **곡선 성분 하나** — 편집·저장·도면이 모두 이 값을 본다.
|
|
|
|
사용자 확정(2026-09-07) — 계획노선은 「직선 > 곡선 > 직선」이고, 사용자가 잡는 것은
|
|
**곡선 시작·끝점**(직선이 곡선에 닿는 자리)이며 필요하면 반지름을 직접 바꾼다.
|
|
그러려면 정점 목록만으로는 모자라 이 성분을 정본에 남겨야 한다.
|
|
|
|
· `apex` — 앞뒤 직선을 늘려 만나는 자리(교각점). **반지름을 바꿔도 여기는 안 움직인다**
|
|
(「곡선 반지름 변경 시 주변 직선 각도 구속」이 그 뜻이다 — 직선이 고정이므로 접선점만
|
|
미끄러진다).
|
|
· `start`·`end` — 곡선 시작·끝점. 사용자가 잡는 손잡이다. 끌면 그쪽 직선 각도와
|
|
반지름이 함께 바뀐다.
|
|
"""
|
|
|
|
apex: tuple[float, float]
|
|
radius_m: float
|
|
tangent_m: float
|
|
inner_angle_deg: float
|
|
start: tuple[float, float]
|
|
end: tuple[float, float]
|
|
node_first: int
|
|
"""이 곡선이 대신하는 꺾임점 구간(첫·끝) — 편집이 어느 노드를 건드리는지 알려 준다."""
|
|
node_last: int
|
|
violations: list[str] = field(default_factory=list)
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"apex": [round(self.apex[0], 4), round(self.apex[1], 4)],
|
|
"radius_m": round(self.radius_m, 3),
|
|
"tangent_m": round(self.tangent_m, 3),
|
|
"inner_angle_deg": round(self.inner_angle_deg, 2),
|
|
"start": [round(self.start[0], 4), round(self.start[1], 4)],
|
|
"end": [round(self.end[0], 4), round(self.end[1], 4)],
|
|
"node_first": self.node_first,
|
|
"node_last": self.node_last,
|
|
"violations": list(self.violations),
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class PlannedPolyline:
|
|
"""폴리라인화 결과. `nodes` 는 편집 대상, `vertices` 는 그리고 계산에 쓰는 선."""
|
|
|
|
nodes: list[RouteNode]
|
|
vertices: list[tuple[float, float]]
|
|
curves: list[RouteCurve] = field(default_factory=list)
|
|
"""직선 사이에 놓인 곡선 성분들 — 순서대로. 편집 손잡이가 이것을 그린다."""
|
|
|
|
@property
|
|
def curve_count(self) -> int:
|
|
return len(self.curves)
|
|
|
|
@property
|
|
def violation_count(self) -> int:
|
|
return sum(1 for node in self.nodes if node.violations)
|
|
|
|
|
|
def _curve_runs(
|
|
cleaned: list[tuple[float, float]],
|
|
) -> list[tuple[int, int]]:
|
|
"""**한 곡선으로 묶을 꺾임점 구간**을 고른다 — `[(첫 꺾임점, 끝 꺾임점)]`.
|
|
|
|
왜 묶나(2026-09-07 실측) — 꺾임점 하나마다 원호를 끼우면 **긴 완만한 곡선을 만들 수 없다**.
|
|
실제로 반지름 40m 짜리 원호를 점으로 흉내 내 넣어 봤더니, 그 곡선이 꺾임점 세 개로 쪼개져
|
|
각각에 작은 원호가 들어갔고 반지름을 키울수록 원본에서 **멀어졌다**(평균 벗어남
|
|
1.56m → 1.89m). 한 곡선은 한 원호여야 한다 — 사용자가 말한 「직선 > 곡선 > 직선」이 그것이다.
|
|
|
|
묶는 규칙: 곡선 대상(내각이 기준 미만)이면서 **도는 방향이 같은** 꺾임점이 이어지면 한 묶음.
|
|
"""
|
|
runs: list[tuple[int, int]] = []
|
|
index = 1
|
|
while index < len(cleaned) - 1:
|
|
inner = _inner_angle_deg(cleaned[index - 1], cleaned[index], cleaned[index + 1])
|
|
if inner >= CURVE_GROUP_INNER_ANGLE_DEG:
|
|
index += 1
|
|
continue
|
|
sign = _turn_sign(cleaned[index - 1], cleaned[index], cleaned[index + 1])
|
|
end = index
|
|
while end + 1 < len(cleaned) - 1:
|
|
following = end + 1
|
|
inner_next = _inner_angle_deg(
|
|
cleaned[following - 1], cleaned[following], cleaned[following + 1]
|
|
)
|
|
if inner_next >= CURVE_GROUP_INNER_ANGLE_DEG:
|
|
break
|
|
if (
|
|
_turn_sign(cleaned[following - 1], cleaned[following], cleaned[following + 1])
|
|
!= sign
|
|
):
|
|
break
|
|
end = following
|
|
# 곡선을 생략하지 않는다(2026-09-07 사용자 지시) — 꺾이는 자리는 전부 곡선이다.
|
|
runs.append((index, end))
|
|
index = end + 1
|
|
return runs
|
|
|
|
|
|
def _run_worst_gap_m(
|
|
first: int,
|
|
last: int,
|
|
cleaned: list[tuple[float, float]],
|
|
samples: list[tuple[float, float]],
|
|
min_radius_m: float,
|
|
) -> tuple[float, float]:
|
|
"""그 묶음을 **한 곡선**으로 폈을 때 예정노선에서 벗어나는 (최대, 평균) 거리(m).
|
|
|
|
못 끼우면 (무한대, 무한대).
|
|
"""
|
|
entry_from = cleaned[first - 1]
|
|
exit_to = cleaned[last + 1]
|
|
apex = (
|
|
cleaned[first]
|
|
if first == last
|
|
else _line_intersection(entry_from, cleaned[first], cleaned[last], exit_to)
|
|
)
|
|
if apex is None:
|
|
return float("inf"), float("inf")
|
|
inner = _inner_angle_deg(entry_from, apex, exit_to)
|
|
half_tan = math.tan(math.radians(180.0 - inner) / 2)
|
|
available = min(_distance(entry_from, apex), _distance(apex, exit_to)) / 2
|
|
if half_tan <= 1e-9 or available <= 0:
|
|
return float("inf"), float("inf")
|
|
radius = min(
|
|
_fit_radius_m(
|
|
samples, entry_from, apex, exit_to, inner, half_tan, min_radius_m, available / half_tan
|
|
),
|
|
available / half_tan,
|
|
)
|
|
geometry = _arc_geometry(entry_from, apex, exit_to, inner, radius, half_tan)
|
|
if geometry is None:
|
|
return float("inf"), float("inf")
|
|
start, end, center = geometry
|
|
line = [
|
|
entry_from,
|
|
start,
|
|
*_arc_points(center, start, end, _turn_sign(entry_from, apex, exit_to) < 0),
|
|
end,
|
|
exit_to,
|
|
]
|
|
gaps = [
|
|
min(_point_to_segment_m(sample, a, b) for a, b in zip(line, line[1:])) for sample in samples
|
|
]
|
|
return max(gaps), sum(gaps) / len(gaps)
|
|
|
|
|
|
def _split_wide_runs(
|
|
runs: list[tuple[int, int]],
|
|
cleaned: list[tuple[float, float]],
|
|
deduped: list[tuple[float, float]],
|
|
node_indices: list[int],
|
|
min_radius_m: float,
|
|
) -> list[tuple[int, int]]:
|
|
"""묶는 것이 **손해면 도로 쪼갠다** — 한 곡선으로 펴서 원본에서 더 멀어지면 안 묶는다.
|
|
|
|
왜(2026-09-07 실측) — 이어진 꺾임을 한 곡선으로 묶으면 완만한 굴곡은 잘 따라가지만
|
|
(반지름 40m 짜리 시험 곡선: 평균 벗어남 0.385 → 0.250m), 성격이 다른 굴곡이 이어 붙은
|
|
자리를 통째로 묶으면 **최대 벗어남이 3.6m → 15.2m** 로 벌어졌다. 그래서 묶음마다
|
|
「묶었을 때」와 「낱개로 뒀을 때」를 재서 **원본에 더 가까운 쪽**을 고른다.
|
|
"""
|
|
result: list[tuple[int, int]] = []
|
|
for first, last in runs:
|
|
if first == last:
|
|
result.append((first, last))
|
|
continue
|
|
samples = deduped[node_indices[first - 1] : node_indices[last + 1] + 1]
|
|
merged_max, merged_mean = _run_worst_gap_m(first, last, cleaned, samples, min_radius_m)
|
|
apart = [
|
|
_run_worst_gap_m(
|
|
index,
|
|
index,
|
|
cleaned,
|
|
deduped[node_indices[index - 1] : node_indices[index + 1] + 1],
|
|
min_radius_m,
|
|
)
|
|
for index in range(first, last + 1)
|
|
]
|
|
apart_max = max(item[0] for item in apart)
|
|
apart_mean = sum(item[1] for item in apart) / len(apart)
|
|
# 평균이 나으면 묶는다 — 「직선 > 곡선 > 직선」이 사용자가 원한 모양이라 조각을
|
|
# 늘리기보다 한 곡선을 우선한다. 다만 **한 자리라도 크게 벌어지면** 안 묶는다.
|
|
if merged_mean <= apart_mean and merged_max <= max(apart_max, MERGE_MAX_GAP_M):
|
|
result.append((first, last))
|
|
else:
|
|
result.extend((index, index) for index in range(first, last + 1))
|
|
return result
|
|
|
|
|
|
def build_planned_polyline(
|
|
points: list[tuple[float, float]],
|
|
*,
|
|
min_radius_m: float,
|
|
hairpin_min_radius_m: float = 10.0,
|
|
simplify: bool = True,
|
|
curve_flags: list[bool] | None = None,
|
|
radii: list[float | None] | None = None,
|
|
) -> PlannedPolyline:
|
|
"""점 묶음을 계획노선 폴리라인으로 바꾼다.
|
|
|
|
`min_radius_m` 은 설계속도·지형으로 고른 법정 최소곡선반지름이다
|
|
(`config_system_design.FOREST_ROAD_PROFILE_CRITERIA["min_plan_radius_m"]`).
|
|
|
|
**먼저 꺾임점을 뽑는다**(`simplify_to_nodes`) — 예상노선은 조밀한 점군이라 그대로 두면
|
|
곡선을 끼울 접선 자리가 없어 반지름이 뭉개진다(용화 실측: 점마다 끼우면 R 2~6m).
|
|
|
|
`simplify=False` 는 **이미 꺾임점인 것을 넘길 때** 쓴다 — 사용자가 옮긴 노드가 그렇다.
|
|
⚠ 이 갈래가 없으면 [확인]을 누를 때마다 선이 깎인다(2026-09-07 실측: 정점 136 → 134 →
|
|
119, 노드 22 → 21). 원호 점이 섞인 폴리라인을 다시 단순화하면 꺾임점이 조금씩 지워지고,
|
|
그 결과로 만든 폴리라인을 또 단순화하기 때문이다. **단순화는 원본 점군에서 한 번만.**
|
|
|
|
`curve_flags`·`radii` 는 **사용자 편집을 그대로 받는 자리**다(2026-09-07 사용자 지시:
|
|
「r과 직선 삭제나 추가가 있어야 하지 않을까」). 점마다 짝을 이룬다.
|
|
· `curve_flags[i] = False` → 그 자리에 **곡선을 두지 않는다**(곡선 삭제). 직선이 그대로
|
|
꺾인다. 다시 True 로 주면 곡선이 돌아온다(곡선 추가).
|
|
· `radii[i]` → 그 곡선의 반지름을 **그 값으로 못박는다**(R 변경). 없으면 예정노선에
|
|
맞춰 고르거나 법정 하한을 쓴다.
|
|
· 직선을 지우거나 더하는 것은 **점 목록 자체**로 표현된다 — 점을 빼면 앞뒤 직선이 하나로
|
|
합쳐지고, 직선 위에 점을 더하면 둘로 갈리며 그 자리에 곡선이 생긴다.
|
|
편집값이 주어지면 **묶지 않는다** — 사용자가 곡선 하나로 본 것을 임의로 합치면 손잡이가
|
|
사라지기 때문이다.
|
|
"""
|
|
# 편집값은 점과 짝이므로 **함께** 중복을 걸러야 어긋나지 않는다.
|
|
edited = curve_flags is not None or radii is not None
|
|
flags = list(curve_flags) if curve_flags is not None else [True] * len(points)
|
|
given = list(radii) if radii is not None else [None] * len(points)
|
|
flags += [True] * (len(points) - len(flags))
|
|
given += [None] * (len(points) - len(given))
|
|
deduped: list[tuple[float, float]] = []
|
|
kept_flags: list[bool] = []
|
|
kept_radii: list[float | None] = []
|
|
for point, flag, radius_value in zip(points, flags, given):
|
|
if deduped and _distance(deduped[-1], point) <= DUPLICATE_TOLERANCE_M:
|
|
continue
|
|
deduped.append(point)
|
|
kept_flags.append(bool(flag))
|
|
kept_radii.append(radius_value)
|
|
cleaned = simplify_to_nodes(deduped) if simplify else deduped
|
|
if len(cleaned) < 3:
|
|
nodes = [RouteNode(x=x, y=y) for x, y in cleaned]
|
|
return PlannedPolyline(nodes=nodes, vertices=list(cleaned), curves=[])
|
|
|
|
# 반지름을 예정노선에 맞추려면 **꺾임점 사이의 원본 점**이 있어야 한다. 사용자가 옮긴
|
|
# 노드를 받은 경우(`simplify=False`)나 자리를 못 찾는 경우에는 피팅을 건너뛴다.
|
|
node_indices = _node_indices(deduped, cleaned) if simplify else None
|
|
|
|
nodes = [RouteNode(x=x, y=y) for x, y in cleaned]
|
|
for index in range(1, len(cleaned) - 1):
|
|
nodes[index].inner_angle_deg = _inner_angle_deg(
|
|
cleaned[index - 1], cleaned[index], cleaned[index + 1]
|
|
)
|
|
|
|
if edited:
|
|
# 사용자가 손댄 목록 — 묶지 않고 자리마다 하나씩 본다. 곡선을 지운 자리는 뺀다.
|
|
runs = [(index, index) for index in range(1, len(cleaned) - 1) if kept_flags[index]]
|
|
else:
|
|
runs = _curve_runs(cleaned)
|
|
if node_indices is not None:
|
|
runs = _split_wide_runs(runs, cleaned, deduped, node_indices, min_radius_m)
|
|
vertices: list[tuple[float, float]] = [cleaned[0]]
|
|
curves: list[RouteCurve] = []
|
|
cursor = 0 # 아직 선에 안 실은 첫 꺾임점
|
|
|
|
for first, last in runs:
|
|
# 곡선 앞뒤의 **직선**. 묶음 안 꺾임점은 그 곡선이 대신하므로 선에 넣지 않는다.
|
|
entry_from, entry_to = cleaned[first - 1], cleaned[first]
|
|
exit_from, exit_to = cleaned[last], cleaned[last + 1]
|
|
# 두 직선을 늘려 만나는 자리가 이 곡선의 꺾임점(IP)이다. 묶음이 하나면 그 노드 자신.
|
|
apex = (
|
|
cleaned[first]
|
|
if first == last
|
|
else _line_intersection(entry_from, entry_to, exit_from, exit_to)
|
|
)
|
|
node = nodes[first]
|
|
if apex is None: # 두 직선이 나란하다 — 곡선을 못 끼운다.
|
|
for index in range(cursor + 1, last + 1):
|
|
vertices.append(cleaned[index])
|
|
cursor = last
|
|
continue
|
|
|
|
inner = _inner_angle_deg(entry_from, apex, exit_to)
|
|
half_tan = math.tan(math.radians(180.0 - inner) / 2)
|
|
# 접선이 들어갈 자리 — 앞뒤 직선을 이웃 곡선과 나눠 쓰므로 절반까지만.
|
|
available = min(_distance(entry_from, apex), _distance(apex, exit_to)) / 2
|
|
if half_tan <= 1e-9 or available <= 0:
|
|
for index in range(cursor + 1, last + 1):
|
|
vertices.append(cleaned[index])
|
|
cursor = last
|
|
continue
|
|
|
|
# 반지름은 **법정 하한 이상에서 예정노선에 가장 가까운 값**(2026-09-07 사용자 확정).
|
|
# 자리가 모자라면 하한 아래로 줄이되 **막지 않고 위반으로 표시**한다(기존 규칙).
|
|
radius = min_radius_m
|
|
forced = kept_radii[first] if edited and first == last else None
|
|
if forced is not None and forced > 0:
|
|
radius = float(forced) # 사용자가 못박은 반지름 — 맞추지 않고 그대로 쓴다.
|
|
elif node_indices is not None:
|
|
samples = deduped[node_indices[first - 1] : node_indices[last + 1] + 1]
|
|
radius = _fit_radius_m(
|
|
samples,
|
|
entry_from,
|
|
apex,
|
|
exit_to,
|
|
inner,
|
|
half_tan,
|
|
min_radius_m,
|
|
available / half_tan,
|
|
)
|
|
tangent = radius * half_tan
|
|
if tangent > available:
|
|
radius = available / half_tan
|
|
tangent = available
|
|
|
|
geometry = _arc_geometry(entry_from, apex, exit_to, inner, radius, half_tan)
|
|
if radius <= 0 or geometry is None:
|
|
for index in range(cursor + 1, last + 1):
|
|
vertices.append(cleaned[index])
|
|
cursor = last
|
|
continue
|
|
start, end, center = geometry
|
|
|
|
if radius < min_radius_m:
|
|
node.violations.append(f"최소곡선반지름 미달({radius:.1f} < {min_radius_m:.1f}m)")
|
|
if radius < hairpin_min_radius_m:
|
|
node.violations.append(
|
|
f"배향곡선 하한 미달({radius:.1f} < {hairpin_min_radius_m:.1f}m)"
|
|
)
|
|
node.radius_m = radius
|
|
node.tangent_m = tangent
|
|
curves.append(
|
|
RouteCurve(
|
|
apex=apex,
|
|
radius_m=radius,
|
|
tangent_m=tangent,
|
|
inner_angle_deg=inner,
|
|
start=start,
|
|
end=end,
|
|
node_first=first,
|
|
node_last=last,
|
|
violations=list(node.violations),
|
|
)
|
|
)
|
|
|
|
# 곡선 앞의 직선 위 꺾임점들은 그대로 잇는다(이 묶음 앞까지).
|
|
for index in range(cursor + 1, first):
|
|
vertices.append(cleaned[index])
|
|
cursor = last
|
|
|
|
clockwise = _turn_sign(entry_from, apex, exit_to) < 0
|
|
vertices.append(start)
|
|
vertices.extend(_arc_points(center, start, end, clockwise=clockwise))
|
|
vertices.append(end)
|
|
|
|
for index in range(cursor + 1, len(cleaned)):
|
|
vertices.append(cleaned[index])
|
|
return PlannedPolyline(nodes=nodes, vertices=vertices, curves=curves)
|