- 새 B05_Profile_Engine_RouteInitial_Fit: 성긴 최소제곱 맞춤 · 측점 벌점 1 → 1000 · 끝 확인(곡선 수 · MC 측점 어김 0 · R ≥ 최소 R) · 수정 끝 틀 고르기(예정노선 거리 덜 늘어나는 합침) - merged_follow: 합치기 뒤 측점 맞춤 대신 맞춤 · 끝 확인 못 지나면 지금 측점 맞춤으로 물러남 · keep_curves - /route/follow: 수정 전 곡선 수 = 본문 keep_curves 또는 저장 노선 곡선표 · 응답 keep_curves · curve_note - 773 여덟 꼴 평균 최대 26.5 → 6.9 m · 평균 3.80 → 1.24 m · N 유지 8/8 · 측점 어김 0 · R 미달 0 · 물러남 0 - 지킴 시험 test_775_route_fit 새로 · test_758_1 그대로 통과(입력 덧점 정리로 체인 · 추적 같은 선) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
5.7 KiB
Python
131 lines
5.7 KiB
Python
"""예정노선 다시 추적(626-2 · 요구 530) — 고친 예정노선 → 평면선(노드 · 곡선 · 편집표) · 저장 0.
|
|
|
|
계산 자리 ③ 서버 단독 — 체인 초기 폴리라인(`_ensure_planned_initial`)과 같은 함수
|
|
`followed_table`(곡선 최적 · 지형 · 계류 격자 잼 · 758-1 · 못 읽으면 순수 기하) · 추적 전 체인
|
|
예정노선과 같은 간격 조밀화(635-2 —
|
|
성긴 원본 꼭짓점 = 체인 초기선과 같은 선 · 촘촘한 입력은 그대로).
|
|
화면은 받은 노드를 편집기 한 걸음으로 싣고 페이지 [저장] 길로 보냄(고친 예정노선 = 초안 `expected`
|
|
→ `expected_route_edit.csv`). `/route/replan` · `as_initial` · `expected_route.csv` · 초기값 ·
|
|
추천 저장본은 안 건드림.
|
|
|
|
POST /api/projects/{id}/route/follow
|
|
본문 {points: [[x, y], …], crs: "tm" | "lonlat", keep_curves?: int} ("lonlat" = [경도, 위도])
|
|
→ {status, planned, nodes, curves, node_edits, station_snap, min_radius_m,
|
|
curve_count, violation_count, keep_curves, curve_note}
|
|
(GET /route/plan 칸 이름 · 사업지 TM)
|
|
`keep_curves`(775-2) = 수정 전 평면계획선 곡선 수 — 안 주면 저장 노선(수정본 → 초기본)
|
|
곡선표에서 읽음 · 곡선 수가 그와 다르면 `curve_note` 안내 글
|
|
(굽이가 모자람 · 맞춤 못 해 지금 측점 맞춤).
|
|
400 점 2 미만 · 404 프로젝트 없음 · 409 좌표계 모름(lonlat)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
from typing import Any, Literal
|
|
from uuid import UUID
|
|
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
|
|
from B05_Profile.B05_Profile_Repository import get_surface_crs_epsg
|
|
from B05_Profile.B05_Profile_Route_Polyline_Write import _read_curves, followed_table
|
|
from B05_Profile.B05_Profile_Router_FollowLand import follow_land
|
|
from B05_Profile.B05_Profile_Router_Replan import (
|
|
_PROJECT_PATH_MISSING,
|
|
_plan_criteria,
|
|
_project_paths,
|
|
router,
|
|
)
|
|
from common_util.common_util_crs import lonlat_to_tm
|
|
from common_util.common_util_route_geometry import (
|
|
densify_route,
|
|
planned_route_initial_path,
|
|
planned_route_working_path,
|
|
)
|
|
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,
|
|
)
|
|
|
|
|
|
class RouteFollowRequest(BaseModel):
|
|
points: list[tuple[float, float]]
|
|
crs: Literal["tm", "lonlat"] = "tm"
|
|
keep_curves: int | None = None
|
|
|
|
|
|
def saved_curve_count(project_root: Path) -> int | None:
|
|
"""저장 노선 곡선 수(수정본 → 초기본 · 775-2) · 곡선표 없으면 None."""
|
|
for path in (
|
|
planned_route_working_path(project_root),
|
|
planned_route_initial_path(project_root),
|
|
):
|
|
if path.is_file():
|
|
return len(_read_curves(path)) or None
|
|
return None
|
|
|
|
|
|
def follow_route_line(
|
|
points: list[tuple[float, float]],
|
|
radius_m: float,
|
|
land: Any = None,
|
|
keep_curves: int | None = None,
|
|
) -> dict[str, Any]:
|
|
"""점 묶음(TM) → 예정노선 추적 평면선 — 파일 꼴(csv 4자리)과 같은 값 · `land` = 곡선 최적 ·
|
|
`keep_curves` = 지킬 곡선 수(지형 있을 때만)."""
|
|
spacing = ROUTE_DIRECT_LINK_CELL_FACTOR * ROUTE_GRID_RES_M * ROUTE_PLANNED_DENSIFY_SAFETY
|
|
result, table = followed_table(densify_route(points, spacing), radius_m, land, keep_curves)
|
|
note = None
|
|
if land is not None and keep_curves and result.curve_count != keep_curves:
|
|
note = (
|
|
f"수정 전 곡선 {keep_curves}개를 지키지 못해 {result.curve_count}개로 그었습니다"
|
|
" — 굽이가 모자라거나 측점 맞춤으로 물러남."
|
|
)
|
|
return {
|
|
"planned": [[round(x, 4), round(y, 4)] for x, y in result.vertices],
|
|
"nodes": [node.as_dict() for node in result.nodes],
|
|
"curves": table["curves"],
|
|
"node_edits": table.get("edits"),
|
|
"station_snap": table["station_snap"],
|
|
"min_radius_m": round(radius_m, 2),
|
|
"curve_count": result.curve_count,
|
|
"violation_count": result.violation_count,
|
|
"keep_curves": keep_curves if land is not None else None,
|
|
"curve_note": note,
|
|
}
|
|
|
|
|
|
@router.post("/{project_id}/route/follow", response_model=None)
|
|
async def follow_route(project_id: UUID, body: RouteFollowRequest) -> dict[str, Any] | JSONResponse:
|
|
"""고친 예정노선을 다시 추적한 평면선 — 아무것도 안 씀(626-2)."""
|
|
if len(body.points) < 2:
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"status": "error", "message": "예정노선은 점이 2개 이상이어야 합니다."},
|
|
)
|
|
paths = await _project_paths(project_id)
|
|
if paths is None:
|
|
return JSONResponse(status_code=404, content=_PROJECT_PATH_MISSING)
|
|
points = [(float(x), float(y)) for x, y in body.points]
|
|
if body.crs == "lonlat":
|
|
async with get_db_pool().acquire() as connection:
|
|
epsg = await get_surface_crs_epsg(connection, project_id, 0)
|
|
if epsg is None:
|
|
return JSONResponse(
|
|
status_code=409,
|
|
content={
|
|
"status": "error",
|
|
"message": "사업지 좌표계를 몰라 경위도를 바꿀 수 없습니다.",
|
|
},
|
|
)
|
|
points = [lonlat_to_tm(lon, lat, epsg) for lon, lat in points]
|
|
radius_m = (await _plan_criteria(project_id))[0]
|
|
land = await follow_land(project_id, paths[0])
|
|
keep = body.keep_curves or await asyncio.to_thread(saved_curve_count, paths[0])
|
|
line = await asyncio.to_thread(follow_route_line, points, radius_m, land, keep)
|
|
return {"status": "success", "project_id": str(project_id), **line}
|