Files
Aislo/B05_Profile/B05_Profile_Router_RouteFollow.py
T
eomsangdonandClaude Opus 5.5 c187f30e8e feat(758-1): 예정노선 따라가기에도 토공 최소 곡선 최적 — 큰 R · 같은 쪽 곡선 합치기 · 직접 잼 측점 맞춤
- 새 `B05_Profile_Engine_RouteInitial_Follow.merged_follow` — 예정노선 폴리라인 → 큰 R(widen) → 꺾임만 곡선 → 같은 쪽 합치기(merge_same_side · 노선 횡단 토공 +10 % 멈춤) → 곡선 MC 일반측점 맞춤 · 잼 = 토공 최소와 같은 Guard(지형 · 계류 격자) · 같은 입력 기억
- 한 함수 — 체인 처음 · /route/plan · /route/replan(`initial_land` · 초기선 낡을 때만 지형 읽음) · /route/follow(`follow_land`) · 추천 「예상노선 추적」(follow_line · 묶음 격자 재사용)
- 지형 못 읽으면 옛 길(순수 기하) · 기존 확정 노선 · 초기선 · 저장본은 안 바꿈(다음 체인 · 끌 때부터)
- 화면 — 따라가기 뒤 지금 추천 = 예상노선 추적(「토공 최소」 꼬리표 남던 흠) · 걸음 사진에 실려 되돌리기 짝
- 엔진 합치기 잣대 셈(cross_total · cross_delta)은 새 파일 한 벌로 옮김(엔진 줄 줄어듦)
- 시험: test_758_1 새 지킴 · test_383_1 은 체인 초기선도 같은 지형을 넘기게 고침(추천 추적 = 곡선 최적이라 순수 기하와 달라짐) · test_626_3 은 onFollow 새 꼴로 글 대조 고침
- 검증_sub1: 곡선 25 → 16 · 측점 16/16 · 예정노선과 최대 10.6 m · 끌 때 1.4~1.9 s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 08:38:17 +09:00

97 lines
4.3 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"} ("lonlat" = [경도, 위도])
→ {status, planned, nodes, curves, node_edits, station_snap, min_radius_m,
curve_count, violation_count} (GET /route/plan 칸 이름 · 사업지 TM)
400 점 2 미만 · 404 프로젝트 없음 · 409 좌표계 모름(lonlat)
"""
from __future__ import annotations
import asyncio
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 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
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"
def follow_route_line(
points: list[tuple[float, float]], radius_m: float, land: Any = None
) -> dict[str, Any]:
"""점 묶음(TM) → 예정노선 추적 평면선 — 파일 꼴(csv 4자리)과 같은 값 · `land` = 곡선 최적."""
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)
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,
}
@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])
line = await asyncio.to_thread(follow_route_line, points, radius_m, land)
return {"status": "success", "project_id": str(project_id), **line}