"""경위도 ↔ 사업지 좌표 묶음 변환(635-5 · 요구 546) — B05 위치 찾기 이동 · 속성 판 경위도. 계산 자리 ③ 서버 단독 · 읽기만. 사업지 좌표계 = 작업 좌표계 창구(`resolve_project_crs` · 설계 노선 `crs_input` 먼저 · WKT 원문 그대로 = 지역 TOWGS84 보존) · pyproj 한 번에 여러 점. 화면은 찾기 · 노드 고름 때만 부름(끌기 중 아님). POST /api/projects/{id}/route/coords {to: "plan" | "lonlat", points: [[a, b], …]} → {points: [[a, b], …]} plan = 경위도 [lon, lat] → [x, y] m · lonlat = 반대 404 프로젝트 없음 · 422 점 꼴 틀림 """ from __future__ import annotations import asyncio from functools import lru_cache from pathlib import Path from typing import Literal from uuid import UUID from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field from pyproj import Transformer from B05_Profile.B05_Profile_Router_Replan import _project_paths router = APIRouter(prefix="/api/projects", tags=["B05 Route Coords"]) MAX_POINTS = 5000 class CoordsRequest(BaseModel): to: Literal["plan", "lonlat"] points: list[tuple[float, float]] = Field(max_length=MAX_POINTS) def project_crs_text(root: Path) -> str: """작업 좌표계 pyproj 입력(`EPSG:n` · WKT) — 라이다 `project_epsg` 와 같은 사다리.""" from common_util.common_util_crs import resolve_project_crs from common_util.common_util_route_geometry import load_design_route planned = load_design_route(root) return resolve_project_crs(root, route_crs_input=planned.crs_input if planned else None) @lru_cache(maxsize=32) def _transformer(crs_text: str, to: str) -> Transformer: pair = ("EPSG:4326", crs_text) if to == "plan" else (crs_text, "EPSG:4326") return Transformer.from_crs(*pair, always_xy=True) def convert(crs_text: str, to: str, points: list[tuple[float, float]]) -> list[list[float]]: """점 묶음 변환 — 경위도 순서 [lon, lat] · 사업지 [x 동, y 북].""" if not points: return [] xs, ys = zip(*points) a, b = _transformer(crs_text, to).transform(xs, ys) return [[float(p), float(q)] for p, q in zip(a, b)] @router.post("/{project_id}/route/coords", response_model=None) async def route_coords(project_id: UUID, body: CoordsRequest) -> dict: paths = await _project_paths(project_id) if paths is None: raise HTTPException(status_code=404, detail="프로젝트 저장 경로를 찾을 수 없습니다.") crs_text = await asyncio.to_thread(project_crs_text, paths[0]) points = await asyncio.to_thread(convert, crs_text, body.to, body.points) return {"points": points}