- /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>
109 lines
4.7 KiB
Python
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,
|
|
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
|
|
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]]:
|
|
"""체인이 자른 예정노선 파일 양끝에서 자름(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)
|
|
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,
|
|
}
|