Files
Aislo/B05_Profile/B05_Profile_Router_PlanSource.py
T
eomsangdonandClaude Opus 5.5 57a6c0ee22 fix(765-8): 벌마다 평면노선 따로 — 받은 노선을 그 벌 지형으로 잘라 벌 예정노선으로 두고 따라감(요구 612)
- 벌 예정노선 terrain_route.csv(벌 몫) — 체인이 받은 노선을 그 지형으로 잘라 한 번 적음 · 초기 폴리라인(merged_follow) · 화면 예상노선 · 고칠 선 끝이 이것을 바탕으로(route_source_path · 없으면 공유 예상노선)
- 공유 예상노선 expected_route.csv = 설계 지형으로 자른 하나(추천 열쇠 그대로) · 고친 예정노선 expected_route_edit.csv 는 벌 몫(다른 벌에 안 번짐)
- 검증_sub5: 라이다 벌 1,352.9 m · 곡선 19(옛 값과 같음) · 수치지형도 벌 2,110.2 m · 곡선 28(예정노선 끝까지) · 칩 왕복 · 벌마다 [초기화] 값 같음 · 공유 예상노선 그대로
- 시험 test_765_design_sets — 벌 몫 가름에 벌 예정노선 · 고친 예정노선 · 벌 예정노선 자름 · 초기 폴리라인 바탕 둘 덧 · 지운 csf 필터 글자를 ptd · classification 으로(637 · 763_3 · 765)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:56:10 +09:00

109 lines
4.7 KiB
Python

"""받은 예정노선 원본 꼭짓점(635-2 · 요구 546) — B05 「예정노선 수정」이 고칠 선.
계산 자리 ③ 서버 단독 · 읽기만(파일 안 만듦). 원본 = 올린(받은) 노선 파일 꼭짓점을 사업지 좌표로 ·
사용 범위(`projects.route_start_m · route_end_m`) · 지표면 트림까지 — 체인 예정노선
(`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
→ {status, expected_source: [[x, y], …], spacing_m} (원본 없음 = 빈 목록)
404 프로젝트 없음
"""
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 (
clip_route_by_chainage,
load_design_route,
read_planned_route_csv,
route_source_path,
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
from config.config_system import (
ROUTE_DIRECT_LINK_CELL_FACTOR,
ROUTE_GRID_RES_M,
ROUTE_PLANNED_DENSIFY_SAFETY,
)
router = APIRouter(prefix="/api/projects", tags=["B05 Plan Source"])
#: 체인 예정노선 조밀화 간격(`load_design_route` 와 같은 값)
SPACING_M = ROUTE_DIRECT_LINK_CELL_FACTOR * ROUTE_GRID_RES_M * ROUTE_PLANNED_DENSIFY_SAFETY
def source_vertices(
root: Path, surface: dict[str, Any] | None, route_range: tuple | None
) -> list[list[float]]:
"""원본 꼭짓점 — 사업지 좌표 · 범위 · 지표면 트림(지표면을 못 열면 트림 없이) · 조밀화 없음."""
planned = load_design_route(root, None, route_range)
if planned is None:
return []
points = [(v.x, v.y) for v in planned.vertices]
if surface:
try:
sampler = build_surface_sampler(
root / "B04_PreProcess" / "models",
str(surface["source_filter"]),
str(surface["method"]),
bool(surface["smooth"]),
)
except (FileNotFoundError, KeyError, OSError):
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]]:
"""체인이 자른 벌 예정노선(765-8) 양끝에서 자름(755-3) — 파일 없음 · 2점 미만 = 그대로."""
path = route_source_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)
if paths is None:
return JSONResponse(status_code=404, content=_PROJECT_PATH_MISSING)
async with get_db_pool().acquire() as connection:
surface = await get_surface_confirmation_params(connection, str(project_id))
async with connection.cursor() as cursor:
await cursor.execute(
"SELECT route_start_m, route_end_m FROM projects WHERE id = %s",
(str(project_id),),
)
row = await cursor.fetchone()
vertices = await asyncio.to_thread(
source_vertices, paths[0], surface, (row[0], row[1]) if row else None
)
return {
"status": "success",
"project_id": str(project_id),
"expected_source": vertices,
"spacing_m": SPACING_M,
}