"""계획노선 폴리라인화 — 원호가 접선과 실제로 맞물리는지 수치로 본다. 왜 (2026-09-06 사용자 지시) — 예상노선은 폴리라인이 아니라 점 묶음이라, 계획노선은 그것을 복사해 **폴리라인으로 바꾼 것**이어야 한다. 곡선 기준은 지식DB(별표2 Ⅰ.2.다) 값을 쓴다. """ import math from common_util.common_util_route_polyline import ( build_planned_polyline, dedupe_points, ) def _dist(a, b): return math.hypot(b[0] - a[0], b[1] - a[1]) def test_붙어_있는_점은_하나로(): points = [(0, 0), (0.1, 0), (0.2, 0), (10, 0)] assert dedupe_points(points) == [(0, 0), (10, 0)] def test_펴진_자리에도_곡선을_둔다(): """내각 175° — **곡선 생략을 안 한다**(2026-09-07 사용자 지시). 별표2 는 「내각 155° 이상이면 곡선을 두지 않을 수 있다」고 하지만, 그러면 조금 폈다는 이유로 곡선이 사라져 **사용자가 평면 노선을 고칠 때** 잡을 손잡이가 없어진다. 그래서 꺾이는 자리에는 늘 곡선을 둔다. 아주 완만하므로 원본과 거의 겹친다. """ at = (100.0, 100.0 * math.tan(math.radians(5))) result = build_planned_polyline([(0.0, 0.0), at, (200.0, 0.0)], min_radius_m=15.0) assert result.curve_count == 1 # 꺾임점은 선 위에 없다 — 아주 완만한 원호가 그 자리를 대신한다. assert all(_dist(v, at) > 1e-9 for v in result.vertices) # 그래도 원본에서 크게 벗어나지 않는다. assert max(abs(v[1] - at[1] * min(v[0], 200 - v[0]) / 100.0) for v in result.vertices) < 3.0 def test_직각_꺾임에_원호가_들어간다(): """내각 90° — 곡선 대상. R 15m 이면 접선 15m, 양쪽 구간 100m 라 자리는 넉넉하다.""" result = build_planned_polyline([(0.0, 0.0), (100.0, 0.0), (100.0, 100.0)], min_radius_m=15.0) node = result.nodes[1] assert result.curve_count == 1 assert node.radius_m == 15.0 assert math.isclose(node.tangent_m, 15.0, rel_tol=1e-6) # R·tan(45°) = R assert not node.violations # 접선점이 꺾임점에서 tangent 만큼 떨어져 있어야 한다. start = (100.0 - 15.0, 0.0) end = (100.0, 15.0) assert any(_dist(v, start) < 1e-6 for v in result.vertices) assert any(_dist(v, end) < 1e-6 for v in result.vertices) # 꺾임점 자체는 선 위에 없다 — 원호가 대신 지나간다. assert all(_dist(v, (100.0, 100.0 - 100.0)) > 1e-6 for v in result.vertices[:1]) assert all(_dist(v, (100.0, 0.0)) > 1e-9 for v in result.vertices) # 원호 위 점은 전부 중심에서 R 만큼 떨어져 있어야 한다(중심 = (85, 15)). center = (85.0, 15.0) arc = [v for v in result.vertices if 1e-6 < _dist(v, start) and 1e-6 < _dist(v, end)] arc = [v for v in arc if abs(_dist(v, center) - 15.0) < 1.0] assert arc, "원호 점이 없다" for point in arc: assert math.isclose(_dist(point, center), 15.0, abs_tol=1e-6) def test_자리가_모자라면_반지름을_줄이고_위반으로_표시(): """구간이 10m 뿐이라 R 15m 접선(15m)이 안 들어간다 — 줄이되 막지 않는다.""" result = build_planned_polyline([(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)], min_radius_m=15.0) node = result.nodes[1] assert node.radius_m is not None assert node.radius_m < 15.0 assert math.isclose(node.tangent_m, 5.0, rel_tol=1e-6) # 구간 10m 의 절반 assert any("최소곡선반지름 미달" in text for text in node.violations) assert result.violation_count == 1 def test_배향곡선_하한도_표시된다(): result = build_planned_polyline([(0.0, 0.0), (6.0, 0.0), (6.0, 6.0)], min_radius_m=15.0) node = result.nodes[1] assert any("배향곡선 하한 미달" in text for text in node.violations) def test_노드는_원본_점을_그대로_들고_있다(): """폴리라인화는 **모양 정리**일 뿐 — 제어점(IP)은 원본 자리에 그대로 있어야 한다.""" points = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (200.0, 100.0)] result = build_planned_polyline(points, min_radius_m=15.0) assert [(node.x, node.y) for node in result.nodes] == points def test_점이_둘_이하면_그대로_돌려준다(): assert build_planned_polyline([(0.0, 0.0), (10.0, 0.0)], min_radius_m=15.0).vertices == [ (0.0, 0.0), (10.0, 0.0), ] assert build_planned_polyline([], min_radius_m=15.0).vertices == [] def test_완만한_긴_굴곡이_삼켜지지_않는다(): """허용오차보다 얕은 S 자 굴곡 — 노드 간격 상한이 없으면 통째로 지워진다. 2026-09-06 실측 재현: 4.5km S자 노선이 노드 20개·곡선 2곳으로 뭉개졌다. 편차 3m(허용오차 4m 아래)짜리 완만한 굴곡을 400m 에 걸쳐 만든다. """ from common_util.common_util_route_polyline import simplify_to_nodes points = [(float(x), 3.0 * math.sin(math.pi * x / 200.0)) for x in range(0, 801, 4)] # 상한이 없으면(되뽑기 끔) 굴곡이 지워져 양 끝만 남는다. flat = simplify_to_nodes(points, 4.0, max_spacing_m=0.0) assert len(flat) <= 3, flat # 상한 100m 를 두면 그 구간을 다시 뽑아 굴곡이 살아난다. kept = simplify_to_nodes(points, 4.0, max_spacing_m=100.0) assert len(kept) > len(flat) spans = [math.hypot(b[0] - a[0], b[1] - a[1]) for a, b in zip(kept, kept[1:])] # 상한은 **되뽑기를 거는 방아쇠**이지 딱 잘라 지키는 값이 아니다 — 겹 수(MAX_REFINE_DEPTH) # 안에서 허용오차를 반씩 낮춰 뽑으므로 조금 넘길 수 있다. 확인할 것은 「지워지지 않았다」. assert max(spans) <= 100.0 * 1.2, spans assert len(kept) >= 8, kept def test_되뽑기는_원본_점만_남긴다(): """되뽑아도 새 점을 만들지 않는다 — 노드는 언제나 원본 자리다.""" from common_util.common_util_route_polyline import simplify_to_nodes points = [(float(x), 3.0 * math.sin(math.pi * x / 200.0)) for x in range(0, 801, 4)] kept = simplify_to_nodes(points, 4.0, max_spacing_m=100.0) assert all(point in points for point in kept)