diff --git a/B05_Profile/B05_Profile_Router_PlanSource.py b/B05_Profile/B05_Profile_Router_PlanSource.py index e4fdcebb2..7ce38b51b 100644 --- a/B05_Profile/B05_Profile_Router_PlanSource.py +++ b/B05_Profile/B05_Profile_Router_PlanSource.py @@ -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) diff --git a/resources/tester/test_635_2_plan_context.py b/resources/tester/test_635_2_plan_context.py index dde01b129..048e79f55 100644 --- a/resources/tester/test_635_2_plan_context.py +++ b/resources/tester/test_635_2_plan_context.py @@ -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]],