fix(755-3): 받은 노선 원본 꼭짓점을 체인 예정노선 파일 양끝에서 자름 — 지표면 넓어진 뒤 280 m 긴 흠

- /route/plan/source = 지표면 트림 뒤 expected_route.csv 시점 · 종점 투영 자리에서 자름(파일 없음 = 그대로) · 0.1 mm 반올림 겹친 점 하나만
- 검증_sub2 source 1,377.4 → 1,097.4 m(= csv) · follow 1,356.5 → 1,080.2 m · 검증_sub1 은 전후 같음(1,097.4)
- 시험 test_635_2 양끝 자름 · 파일 없음 그대로 한 건 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 23:21:40 +09:00
co-authored by Claude Opus 5.5
parent ad9062dec7
commit 8b3560c4ce
2 changed files with 51 additions and 2 deletions
+26 -2
View File
@@ -2,7 +2,9 @@
계산 자리 ③ 서버 단독 · 읽기만(파일 안 만듦). 원본 = 올린(받은) 노선 파일 꼭짓점을 사업지 좌표로 ·
사용 범위(`projects.route_start_m · route_end_m`) · 지표면 트림까지 — 체인 예정노선
(`load_design_route`)과 같은 차례에서 **조밀화만 뺌**. 고친 꼭짓점은 `POST /route/follow` 가
(`load_design_route`)과 같은 차례에서 **조밀화만 뺌**. 끝 = 체인이 자른 `expected_route.csv`
양끝(755-3 · 지표면이 그 뒤 넓어져도 화면 예상노선 · 저장 csv 와 같은 길이).
고친 꼭짓점은 `POST /route/follow` 가
같은 간격(`spacing_m`)으로 조밀화한 뒤 추적 → 안 고친 원본 = 체인 초기선과 거리 0(`test_635_2`).
GET /api/projects/{id}/route/plan/source
@@ -13,15 +15,23 @@
from __future__ import annotations
import asyncio
import math
from pathlib import Path
from typing import Any
from uuid import UUID
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from shapely.geometry import LineString, Point
from B05_Profile.B05_Profile_Router_Replan import _PROJECT_PATH_MISSING, _project_paths
from common_util.common_util_route_geometry import load_design_route, trim_route_to_surface
from common_util.common_util_route_geometry import (
clip_route_by_chainage,
expected_route_csv_path,
load_design_route,
read_planned_route_csv,
trim_route_to_surface,
)
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
from common_util.common_util_surface_sampler import build_surface_sampler
from config.config_db import get_db_pool
@@ -57,9 +67,23 @@ def source_vertices(
sampler = None
if sampler is not None:
points = trim_route_to_surface(points, sampler)
points = clip_to_expected(root, points)
return [[float(x), float(y)] for x, y in points] if len(points) >= 2 else []
def clip_to_expected(root: Path, points: list[tuple[float, float]]) -> list[tuple[float, float]]:
"""체인이 자른 예정노선 파일 양끝에서 자름(755-3) — 파일 없음 · 2점 미만 = 그대로."""
path = expected_route_csv_path(root)
stored = read_planned_route_csv(path) if path.is_file() else None
if stored is None or len(stored.vertices) < 2 or len(points) < 2:
return points
line = LineString(points)
ends = [line.project(Point(v.x, v.y)) for v in (stored.vertices[0], stored.vertices[-1])]
clipped = clip_route_by_chainage(points, min(ends), max(ends))
# 파일 좌표 반올림(0.1 mm) = 끝이 꼭짓점 바로 앞에 떨어짐 — 1 mm 안 겹친 점은 하나만
return [p for i, p in enumerate(clipped) if not i or math.dist(p, clipped[i - 1]) > 1e-3]
@router.get("/{project_id}/route/plan/source", response_model=None)
async def read_plan_source(project_id: UUID) -> dict[str, Any] | JSONResponse:
paths = await _project_paths(project_id)
@@ -133,6 +133,31 @@ def test_plan_source_skips_densify_keeps_trim(tmp_path: Path, monkeypatch) -> No
assert asyncio.run(source.read_plan_source(uuid4())).status_code == 404
def test_plan_source_clips_to_expected_csv_ends(tmp_path: Path, monkeypatch) -> None:
"""755-3 — 지표면이 넓어져 트림이 덜 잘라도 체인 예정노선 파일 양끝에서 자름(같은 길이)."""
monkeypatch.setattr(
source,
"load_design_route",
lambda *a: SimpleNamespace(vertices=[SimpleNamespace(x=x, y=y) for x, y in SPARSE]),
)
monkeypatch.setattr(source, "build_surface_sampler", lambda *a: object())
monkeypatch.setattr(source, "trim_route_to_surface", lambda points, sampler: points)
surface = {"source_filter": "classification", "method": "dtm", "smooth": True}
dense = densify_route(SPARSE[3:30], source.SPACING_M)
write_route_csv(expected_route_csv_path(tmp_path), [{"x": x, "y": y} for x, y in dense])
got = source.source_vertices(tmp_path, surface, None)
def length(points):
return sum(math.dist(a, b) for a, b in zip(points, points[1:]))
assert abs(length(got) - length(dense)) < 1e-3 # 파일 좌표 0.1 mm 반올림
assert math.dist(got[0], SPARSE[3]) < 1e-3 and math.dist(got[-1], SPARSE[29]) < 1e-3
assert len(got) == 27 # 끝 = 꼭짓점 바로 앞 · 겹친 점 없음
# 파일 없음 = 그대로
expected_route_csv_path(tmp_path).unlink()
assert len(source.source_vertices(tmp_path, surface, None)) == len(SPARSE)
MAIN = {
"id": "main",
"vertices": [[200000.0, 450000.0], [200100.0, 450000.0], [200200.0, 450000.0]],