- 세트 연결 = 638-1 그대로(B 하나 = 세트 하나 · 열쇠만 세트) - 새 D04_PlanConfirm_Context — 같은 예정노선 세트 모두 · 내 세트 · 연도 · 임도 좌표계로 pyproj 변환 · 업체 이름 없음 · 보기 전용 - 새 B05_Profile_Router_PlanSource — 원본 꼭짓점 = 범위 + 지표면 트림 · 조밀화만 뺌(Router_Replan 안 늘림 · 덧칸 대신 새 길) - RouteFollow 추적 전 체인과 같은 3.6 m 조밀화(브레인 허락 · 옛 촘촘 입력은 그대로 = 626-2 같음) - 실측 검증_sub3: 원본 64 점 → follow = 초기선 181 점 거리 0 · 파일 안 바뀜 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
7.4 KiB
Python
184 lines
7.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""635-2 받은 세트 문맥 · 원본 꼭짓점 · 다시 추적 거리 0(요구 546 · 547).
|
|
|
|
- 원본 꼭짓점(조밀화 전 · 범위 · 지표면 트림) → `/route/follow` = 체인 초기선과 같은 선(거리 0) —
|
|
follow 가 체인 예정노선과 같은 간격으로 조밀화한 뒤 추적(촘촘한 옛 입력은 그대로)
|
|
- `GET /route/plan/source` = 원본 꼭짓점 · 조밀화 없음 · 트림 · 범위 넘김
|
|
- `GET /plan-context` = 같은 예정노선 세트 모두 · 내 세트 · 연도 · 임도 좌표계 · 업체 이름 없음 ·
|
|
연결 없으면 linked False · 파일 안 씀
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import math
|
|
import sys
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from uuid import uuid4
|
|
|
|
from pyproj import Transformer
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from helper_plan_file import plan_of, use_plan # noqa: E402
|
|
|
|
from B05_Profile import B05_Profile_Router_PlanSource as source # noqa: E402
|
|
from B05_Profile import B05_Profile_Router_Replan as replan # noqa: E402
|
|
from B05_Profile import B05_Profile_Router_RouteFollow as follow # noqa: E402
|
|
from common_util.common_util_route_geometry import ( # noqa: E402
|
|
densify_route,
|
|
expected_route_csv_path,
|
|
planned_route_initial_path,
|
|
write_route_csv,
|
|
)
|
|
from D04_PlanConfirm import D04_PlanConfirm_Context as context # noqa: E402
|
|
|
|
RADIUS_M = 12.0
|
|
# 성긴 원본 꼭짓점(17 m 안팎 · 굽은 산길) — 사업지 TM
|
|
SPARSE = [(200000.0 + 17 * i, 450000.0 + 30 * math.sin(i / 3)) for i in range(40)]
|
|
|
|
|
|
def _rows(path: Path) -> list[list[float]]:
|
|
return replan._vertices_of(path)
|
|
|
|
|
|
def test_sparse_vertices_follow_equals_chain_initial(tmp_path: Path) -> None:
|
|
"""체인 = 원본 → 조밀화(3.6 m) → 예정노선 파일 → 초기 폴리라인 · follow(원본) = 같은 선."""
|
|
dense = densify_route(SPARSE, source.SPACING_M)
|
|
assert len(dense) > 3 * len(SPARSE)
|
|
write_route_csv(expected_route_csv_path(tmp_path), [{"x": x, "y": y} for x, y in dense])
|
|
assert replan._ensure_planned_initial(tmp_path, RADIUS_M)
|
|
initial = _rows(planned_route_initial_path(tmp_path))
|
|
line = follow.follow_route_line(SPARSE, RADIUS_M)["planned"]
|
|
assert len(line) == len(initial)
|
|
assert max(math.dist(a, b) for a, b in zip(line, initial, strict=True)) < 1e-3
|
|
# 촘촘한 옛 입력(체인 파일 점) = 조밀화가 아무것도 안 끼움 · 같은 선
|
|
same = [tuple(p) for p in replan._vertices_of(expected_route_csv_path(tmp_path))]
|
|
assert densify_route(same, source.SPACING_M) == same
|
|
assert follow.follow_route_line(same, RADIUS_M)["planned"] == initial
|
|
|
|
|
|
def test_plan_source_skips_densify_keeps_trim(tmp_path: Path, monkeypatch) -> None:
|
|
seen = {}
|
|
|
|
def load(root, surface, route_range):
|
|
seen["args"] = (root, surface, route_range)
|
|
return SimpleNamespace(vertices=[SimpleNamespace(x=x, y=y) for x, y in SPARSE])
|
|
|
|
def trim(points, sampler):
|
|
return points[2:-2]
|
|
|
|
monkeypatch.setattr(source, "load_design_route", load)
|
|
monkeypatch.setattr(source, "build_surface_sampler", lambda *a: object())
|
|
monkeypatch.setattr(source, "trim_route_to_surface", trim)
|
|
surface = {"source_filter": "classification", "method": "dtm", "smooth": True}
|
|
got = source.source_vertices(tmp_path, surface, (10.0, 500.0))
|
|
assert seen["args"] == (tmp_path, None, (10.0, 500.0)) # 지표면 인자 없이 = 조밀화 안 함
|
|
assert got == [list(p) for p in SPARSE[2:-2]]
|
|
|
|
def broken(*a):
|
|
raise FileNotFoundError("모델 없음")
|
|
|
|
monkeypatch.setattr(source, "build_surface_sampler", broken)
|
|
assert len(source.source_vertices(tmp_path, surface, None)) == len(SPARSE) # 트림 없이
|
|
monkeypatch.setattr(source, "load_design_route", lambda *a: None)
|
|
assert source.source_vertices(tmp_path, surface, None) == []
|
|
|
|
# 끝점 — 경로 없음 404 · 있으면 칸 · 간격
|
|
async def paths(_pid):
|
|
return tmp_path, "s"
|
|
|
|
class _Cursor:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *a):
|
|
return False
|
|
|
|
async def execute(self, *a):
|
|
pass
|
|
|
|
async def fetchone(self):
|
|
return (None, None)
|
|
|
|
class _Conn:
|
|
def cursor(self):
|
|
return _Cursor()
|
|
|
|
class _Pool:
|
|
@asynccontextmanager
|
|
async def acquire(self):
|
|
yield _Conn()
|
|
|
|
async def confirmed(*a):
|
|
return surface
|
|
|
|
monkeypatch.setattr(source, "_project_paths", paths)
|
|
monkeypatch.setattr(source, "get_db_pool", lambda: _Pool())
|
|
monkeypatch.setattr(source, "get_surface_confirmation_params", confirmed)
|
|
monkeypatch.setattr(source, "load_design_route", load)
|
|
monkeypatch.setattr(source, "build_surface_sampler", lambda *a: object())
|
|
body = asyncio.run(source.read_plan_source(uuid4()))
|
|
assert body["expected_source"] == [list(p) for p in SPARSE[2:-2]]
|
|
assert body["spacing_m"] == source.SPACING_M == 3.6
|
|
|
|
async def none(_pid):
|
|
return None
|
|
|
|
monkeypatch.setattr(source, "_project_paths", none)
|
|
assert asyncio.run(source.read_plan_source(uuid4())).status_code == 404
|
|
|
|
|
|
MAIN = {
|
|
"id": "main",
|
|
"vertices": [[200000.0, 450000.0], [200100.0, 450000.0], [200200.0, 450000.0]],
|
|
}
|
|
GAP = {"id": "g", "vertices": [[200300.0, 450000.0], [200400.0, 450000.0]]}
|
|
|
|
|
|
def test_plan_context_sets_mine_crs(tmp_path: Path, monkeypatch) -> None:
|
|
plan = plan_of(
|
|
[MAIN, GAP],
|
|
[{"line": "main", "at_m": 100.0}],
|
|
[
|
|
{"id": "a", "line": "main", "from_m": 0, "to_m": 100, "year": 2027, "set": 1},
|
|
{"id": "b", "line": "main", "from_m": 100, "to_m": 200, "year": 2027, "set": 2},
|
|
{"id": "c", "line": "g", "from_m": 0, "to_m": 100, "year": 2028, "set": 1},
|
|
],
|
|
)
|
|
use_plan(monkeypatch, plan)
|
|
rows = {"link": {"project_id": "D", "piece_id": "b", "plan_name": "예정"}}
|
|
|
|
async def fetch(sql, args):
|
|
assert "a.linked_project_id" in sql and "company" not in sql
|
|
return rows["link"]
|
|
|
|
async def road(_pid):
|
|
return {"storage_path": "x"}
|
|
|
|
monkeypatch.setattr(context, "_fetch_one", fetch)
|
|
monkeypatch.setattr(context.store, "project_row", road)
|
|
monkeypatch.setattr(context, "resolve_stored_project_path", lambda s: str(tmp_path))
|
|
monkeypatch.setattr(context, "resolve_project_crs", lambda root: "EPSG:5174")
|
|
out = asyncio.run(context.plan_context(uuid4()))
|
|
assert (out["linked"], out["mine"], out["year"], out["epsg"]) == (True, "2027-2", 2027, 5174)
|
|
assert [(s["key"], s["mine"], s["year"], len(s["lines"])) for s in out["sets"]] == [
|
|
("2027-1", False, 2027, 1),
|
|
("2027-2", True, 2027, 1),
|
|
("2028-1", False, 2028, 1),
|
|
]
|
|
move = Transformer.from_crs("EPSG:5186", "EPSG:5174", always_xy=True)
|
|
want = [list(move.transform(*p)) for p in ([200100.0, 450000.0], [200200.0, 450000.0])]
|
|
assert out["sets"][1]["lines"][0] == want # 내 세트 = 임도 좌표(Bessel)
|
|
assert "업체" not in str(out) and "email" not in str(out)
|
|
# 옛 업체 열쇠(조각 id) · 세트 열쇠 = 같은 답
|
|
rows["link"] = {**rows["link"], "piece_id": "2027-2"}
|
|
assert asyncio.run(context.plan_context(uuid4()))["mine"] == "2027-2"
|
|
rows["link"] = None
|
|
assert asyncio.run(context.plan_context(uuid4())) == {"status": "success", "linked": False}
|
|
assert not any(tmp_path.iterdir()) # 파일 안 씀
|