- Shift+클릭으로 노선 위 두 점을 찍으면 구간 길이와 종단기울기 표기 (계획서 0-9 ⑤) - 지반고 통로 `POST /route/elevations` 추가 — 종·횡단과 같은 sampler 를 읽기만 함 - 찍는 순간에만 서버를 부름, 끄는 동안에는 안 부름 - 700줄 규정에 맞춰 구간 재기와 [확인] 처리를 `_Measure.ts`·`_Apply.ts` 로 분리 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012jsXWphgRUHAG2mFupSKPX
95 lines
4.1 KiB
Python
95 lines
4.1 KiB
Python
"""계획노선 편집 중 **지반고만** 묻는 가벼운 통로.
|
|
|
|
편집 모달은 [확인] 전까지 아무 계산도 내보내지 않는다(계획서 0-2 확정 7). 다만 두 점을 찍어
|
|
**구간 길이와 종단기울기**를 볼 때(0-9 ⑤)와 한 측점의 **횡단도 미리보기**(⑧)는 지반고가
|
|
있어야 한다. 새 계산이 아니라 **이미 확정된 지표면을 읽기만** 하는 통로라 그 규칙과 부딪히지
|
|
않는다 — 노선을 갈아 끼우지도, 정본을 건드리지도 않는다.
|
|
|
|
표고 조회는 종·횡단 생성기가 쓰는 것과 **같은 sampler**(`build_surface_sampler`)를 연다.
|
|
두 화면이 다른 표고를 보면 같은 자리의 기울기가 갈린다.
|
|
|
|
POST /api/projects/{id}/route/elevations → 점 묶음의 지반고
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from pathlib import Path
|
|
from uuid import UUID
|
|
|
|
import numpy as np
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel, Field
|
|
|
|
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
|
from common_util.common_util_storage import resolve_stored_project_path
|
|
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
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/projects", tags=["B05 Route Terrain"])
|
|
|
|
_MODELS_SUBDIR = Path("B04_PreProcess") / "models"
|
|
#: 한 번에 물을 수 있는 점 수. 구간 재기는 수십 점, 횡단 한 장은 수백 점이면 넉넉하다 —
|
|
#: 상한을 두어 실수로 노선 전체를 밀어 넣는 일을 막는다.
|
|
MAX_POINTS = 4000
|
|
|
|
|
|
class ElevationRequest(BaseModel):
|
|
"""사업지 좌표계(m) 점 묶음 [[x, y], …]."""
|
|
|
|
points: list[tuple[float, float]] = Field(..., min_length=1, max_length=MAX_POINTS)
|
|
|
|
|
|
def _sample(project_root: Path, params: dict, points: list[tuple[float, float]]):
|
|
"""확정 지표면에서 표고를 읽는다. 모델을 못 열면 None."""
|
|
try:
|
|
sampler = build_surface_sampler(
|
|
project_root / _MODELS_SUBDIR,
|
|
str(params["source_filter"]),
|
|
str(params["method"]),
|
|
bool(params["smooth"]),
|
|
)
|
|
except (FileNotFoundError, KeyError, OSError, ValueError) as exc:
|
|
logger.warning("계획노선 편집: 지표면을 열지 못했습니다 — %s", exc)
|
|
return None
|
|
z, valid = sampler.sample_xy(np.asarray(points, dtype=np.float64))
|
|
return z, valid
|
|
|
|
|
|
@router.post("/{project_id}/route/elevations", response_model=None)
|
|
async def read_route_elevations(project_id: UUID, request: ElevationRequest) -> dict | JSONResponse:
|
|
"""점 묶음의 지반고(m)와 유효 여부를 돌려준다.
|
|
|
|
지표면 밖이거나 자료가 없는 자리는 `valid=false` 로 나가고 표고는 `null` 이다 —
|
|
**임의 표고로 메우지 않는다**(sampler 규칙 그대로). 화면은 그 자리를 「모름」으로 낸다.
|
|
"""
|
|
pool = get_db_pool()
|
|
async with pool.acquire() as connection:
|
|
stored = await get_project_storage_relative_path(connection, project_id)
|
|
if not stored:
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={"status": "error", "message": "프로젝트 저장 경로를 찾을 수 없습니다."},
|
|
)
|
|
params = await get_surface_confirmation_params(connection, str(project_id))
|
|
|
|
project_root = Path(resolve_stored_project_path(stored))
|
|
sampled = await asyncio.to_thread(_sample, project_root, params, request.points)
|
|
if sampled is None:
|
|
return JSONResponse(
|
|
status_code=409,
|
|
content={
|
|
"status": "error",
|
|
"message": "확정된 지표면이 없어 지반고를 읽을 수 없습니다.",
|
|
},
|
|
)
|
|
z, valid = sampled
|
|
return {
|
|
"status": "success",
|
|
"project_id": str(project_id),
|
|
"z": [None if not ok else round(float(value), 3) for value, ok in zip(z, valid)],
|
|
"valid": [bool(ok) for ok in valid],
|
|
}
|