feat(D03): 지형 = 사업 전체 한 벌 · 받기 전 범위 API (PLAN 561-2)
- 지형 받기 — 연도 구간마다 models_<연도> 대신 한 벌 models_plan · 범위 = 전체 시점 · 종점(+ 그은 점) 상자 + 여유 ∪ 받은 범위(줄지 않음) · 받은 범위 plan_terrain.json
- 도엽 = 범위 상자에 닿는 도엽 모두(3×3 이웃 대신) · 위성 = 그 도엽 도곽
- 새 GET …/plan/terrain/extent — 상자 네 귀 경위도 · 도엽 도곽 · 칸 수 어림 · 받은 범위 · covered(더 받기 판정) · 내려받기 없음
- POST …/plan/terrain 몸 = {margin_m, extra} · 여유 처음 값 350 m(옛 DEM 폭) · 바닥 300 m · 실패 글에 병합 도엽 매수
- 전체 시점 · 종점 = 임시로 첫 연도 시점 · 끝 연도 종점(_plan_ends · 560-2 받은 뒤 바꿈)
- 시험 530_1 다시 씀 · 530_2_plan_router 범위 · 받기 둘 더함 · 530_2_plan_analyze 옛 연도 DEM 기대 한 줄 고침(한 벌로 바뀜)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
"""D 예정노선 지형 받기(PLAN 530-1) — 가짜 내려받기 · 옛 도엽 유지 · 새 도엽만 · 빠짐 없음.
|
||||
"""D 예정노선 지형 받기(PLAN 530-1 · 561-2) — 사업 전체 한 벌 · 범위 보기 = 내려받기 없음 ·
|
||||
옛 도엽 유지 · 새 도엽만 · 범위는 줄지 않음 · 빠짐 없음.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_530_1_plan_terrain.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -60,56 +60,85 @@ def _zips(root):
|
||||
return sorted(p.stem for p in store_mod.get_project_map_sheets_dir(root).glob("*.zip"))
|
||||
|
||||
|
||||
def test_more_points_keep_old_sheets_and_fetch_only_new(fake) -> None:
|
||||
def _dem_box(seen) -> list[float]:
|
||||
"""가짜 DEM 이 받은 꼭짓점 상자 + B 절취 여유 = 실제 DEM 상자."""
|
||||
(x0, y0), (x1, y1) = seen["dem"][-1][1]
|
||||
m = SHEET_SURFACE_MARGIN_M
|
||||
return [x0 - m, y0 - m, x1 + m, y1 + m]
|
||||
|
||||
|
||||
def test_one_set_and_extend_keeps_old(fake) -> None:
|
||||
root, seen = fake
|
||||
first = terrain.ensure_plan_terrain(root, [{"year": 2026, "start": A, "end": B}], EPSG)
|
||||
first = terrain.ensure_plan_terrain(root, [A, B], EPSG)
|
||||
old = _zips(root)
|
||||
assert sorted(seen["download"]) == old == sorted(first["sheets"]["downloaded"])
|
||||
assert first["satellite"] == "done" and list(first["years"]) == [2026]
|
||||
assert first["satellite"] == "done" and len(first["models"]) == 1
|
||||
assert seen["dem"][-1][0] == "models_plan" # 연도 없는 한 벌
|
||||
m = terrain.DEFAULT_MARGIN_M
|
||||
box = [min(A[0], B[0]) - m, min(A[1], B[1]) - m, max(A[0], B[0]) + m, max(A[1], B[1]) + m]
|
||||
assert _dem_box(seen) == pytest.approx(box) == first["box_m"]
|
||||
assert terrain.received_box(root) == pytest.approx(tuple(box))
|
||||
# 작업 좌표계 .prj — B 좌표계 창구가 읽음
|
||||
assert resolve_project_crs(root) == f"EPSG:{EPSG}"
|
||||
|
||||
# 그은 점이 범위 밖 — 더 받기 = 옛 범위 ∪ 새 범위 · 새 도엽만
|
||||
seen["download"].clear()
|
||||
segs = [{"year": 2026, "start": A, "end": B}, {"year": 2027, "start": B, "end": C}]
|
||||
second = terrain.ensure_plan_terrain(root, segs, EPSG)
|
||||
second = terrain.ensure_plan_terrain(root, [A, B, C], EPSG)
|
||||
now = _zips(root)
|
||||
assert set(old) <= set(now) # 옛 도엽 그대로(prune 없음)
|
||||
assert sorted(seen["download"]) == sorted(set(now) - set(old)) # 새 도엽만 받음
|
||||
assert sorted(seen["download"]) == sorted(set(now) - set(old))
|
||||
assert seen["merge"][-1] == now # 병합 = 옛 ∪ 새
|
||||
# 위성 = 옛 범위 ∪ 새 범위
|
||||
first_box, union = seen["satellite"][0], seen["satellite"][-1]
|
||||
assert union["x"][0] <= first_box["x"][0] and union["x"][1] >= first_box["x"][1]
|
||||
assert union["y"][0] <= first_box["y"][0] and union["y"][1] >= first_box["y"][1]
|
||||
assert [d[0] for d in seen["dem"][-2:]] == ["models_2026", "models_2027"]
|
||||
grown = second["box_m"]
|
||||
assert grown[0] <= box[0] and grown[1] <= box[1] and grown[2] >= C[0] + m
|
||||
first_sat, union = seen["satellite"][0], seen["satellite"][-1]
|
||||
assert union["x"][0] <= first_sat["x"][0] and union["x"][1] >= first_sat["x"][1]
|
||||
assert second["sheets"]["failed"] == {}
|
||||
|
||||
# 같은 지점 다시 — 받기 0 · 위성 건너뜀 · 병합 열쇠 같음
|
||||
# 같은 범위 다시 — 받기 0 · 위성 건너뜀 · 병합 열쇠 같음 · 작은 점이어도 범위 줄지 않음
|
||||
seen["download"].clear()
|
||||
merges = len(seen["merge"])
|
||||
third = terrain.ensure_plan_terrain(root, segs, EPSG)
|
||||
third = terrain.ensure_plan_terrain(root, [A, B], EPSG)
|
||||
assert seen["download"] == [] and third["satellite"] == "skip"
|
||||
assert len(seen["merge"]) == merges
|
||||
assert len(seen["merge"]) == merges and third["box_m"] == grown
|
||||
|
||||
|
||||
def test_far_points_leave_no_gap(fake) -> None:
|
||||
def test_extent_no_download_and_covered(fake) -> None:
|
||||
root, seen = fake
|
||||
before = terrain.plan_extent(root, [A, B], EPSG)
|
||||
assert seen["download"] == [] and before["have"] is None and before["covered"] is False
|
||||
assert before["margin_m"] == terrain.DEFAULT_MARGIN_M and len(before["box"]) == 4
|
||||
x0, y0, x1, y1 = before["box_m"]
|
||||
assert before["cells"] == int((x1 - x0) * (y1 - y0))
|
||||
assert all(not s["stored"] and len(s["ring"]) == 4 for s in before["sheets"])
|
||||
terrain.ensure_plan_terrain(root, [A, B], EPSG)
|
||||
after = terrain.plan_extent(root, [A, B], EPSG)
|
||||
assert after["covered"] and after["have_m"] == after["box_m"]
|
||||
assert all(s["stored"] for s in after["sheets"])
|
||||
assert {s["no"] for s in after["sheets"]} == {s["no"] for s in before["sheets"]}
|
||||
assert not terrain.plan_extent(root, [A, C], EPSG)["covered"] # 그은 점이 밖 = 더 받기
|
||||
# 여유 — 바닥 B 절취 여유 · 빈 값 = 처음 값
|
||||
assert terrain.clamp_margin(100.0) == SHEET_SURFACE_MARGIN_M
|
||||
assert terrain.clamp_margin(None) == terrain.DEFAULT_MARGIN_M
|
||||
assert terrain.clamp_margin(1e9) == terrain.MAX_MARGIN_M
|
||||
|
||||
|
||||
def test_box_sheets_leave_no_gap(fake) -> None:
|
||||
root, _ = fake
|
||||
far = (A[0] + 9000.0, A[1] + 7000.0) # 11 km — 3×3 두 벌이 안 닿는 거리
|
||||
got = terrain.ensure_plan_terrain(root, [{"year": 2026, "start": A, "end": far}], EPSG)
|
||||
far = (A[0] + 9000.0, A[1] + 7000.0) # 11 km
|
||||
got = terrain.ensure_plan_terrain(root, [A, far], EPSG)
|
||||
x0, y0, x1, y1 = got["box_m"]
|
||||
to_wgs = Transformer.from_crs(f"EPSG:{EPSG}", "EPSG:4326", always_xy=True)
|
||||
on_line = []
|
||||
for i in range(201):
|
||||
lon, lat = to_wgs.transform(
|
||||
A[0] + (far[0] - A[0]) * i / 200, A[1] + (far[1] - A[1]) * i / 200
|
||||
)
|
||||
on_line.append((lat, lon))
|
||||
assert set(sheets_for_points(on_line)) <= set(got["sheets"]["merged"])
|
||||
inside = []
|
||||
for i in range(41):
|
||||
for j in range(41):
|
||||
lon, lat = to_wgs.transform(x0 + (x1 - x0) * i / 40, y0 + (y1 - y0) * j / 40)
|
||||
inside.append((lat, lon))
|
||||
assert set(sheets_for_points(inside)) <= set(got["sheets"]["merged"])
|
||||
|
||||
|
||||
def test_dem_outline_covers_corridor() -> None:
|
||||
box = terrain.dem_outline(A, B)
|
||||
reach = terrain.PLAN_CORRIDOR_M + terrain.DEM_EXTRA_M
|
||||
assert box[0][0] - SHEET_SURFACE_MARGIN_M <= min(A[0], B[0]) - reach + 1e-9
|
||||
assert box[1][1] + SHEET_SURFACE_MARGIN_M >= max(A[1], B[1]) + reach - 1e-9
|
||||
samples = terrain.segment_samples(A, C)
|
||||
steps = [math.dist(p, q) for p, q in zip(samples, samples[1:])]
|
||||
assert samples[0] == A and samples[-1] == C and max(steps) <= terrain.SAMPLE_STEP_M
|
||||
def test_empty_surface_raises_and_keeps_no_box(fake, monkeypatch) -> None:
|
||||
root, _ = fake
|
||||
monkeypatch.setattr(terrain, "build_sheet_surface_model", lambda *a, **k: [])
|
||||
with pytest.raises(ValueError, match="사업 지형면을 만들지 못했습니다"):
|
||||
terrain.ensure_plan_terrain(root, [A, B], EPSG)
|
||||
assert terrain.received_box(root) is None
|
||||
|
||||
@@ -56,8 +56,8 @@ def run(tmp_path, hill):
|
||||
|
||||
def test_boundary_chainage_and_line(run) -> None:
|
||||
results = run()
|
||||
# 연도 구간마다 제 DEM · 제 비용면 캐시
|
||||
assert run.calls == [("models_2026", "cost_2026"), ("models_2027", "cost_2027")]
|
||||
# DEM = 사업 전체 한 벌(561-2) · 비용면 캐시는 연도 구간마다
|
||||
assert run.calls == [("models_plan", "cost_2026"), ("models_plan", "cost_2027")]
|
||||
for seg, result in zip(SEGMENTS, results):
|
||||
assert set(result["routes"]) == set(analyze.PLAN_MODES)
|
||||
for route in result["routes"].values():
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""D 분석 API(PLAN 530-2 · 530-3) — GET 이은 누가 · PUT [저장] = 서버 저장본으로 다시 셈 · 모르는 갈래
|
||||
거절 · 끈 지점(points) = 그 지점으로 보기 · 안 이어지면 거절 · [저장] 때만 지점 정본(산림청만) ·
|
||||
꺾임점 고침(543-3) = 서버가 마디로 선 · 지표를 다시 셈 · 누가가 고친 길이로 · GET 은 지문 같은 고침만.
|
||||
"""D 분석 API(PLAN 530-2 · 530-3 · 561-2) — GET 이은 누가 · PUT [저장] = 서버 저장본으로 다시 셈 ·
|
||||
모르는 갈래 거절 · 끈 지점(points) = 그 지점으로 보기 · 안 이어지면 거절 ·
|
||||
[저장] 때만 지점 정본(산림청만) ·
|
||||
꺾임점 고침(543-3) = 서버가 마디로 선 · 지표를 다시 셈 · 누가가 고친 길이로 · GET 은 지문 같은
|
||||
고침만 · 지형 = 전체 시점 · 종점 한 벌 · 범위 보기.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_530_2_plan_router.py -q
|
||||
"""
|
||||
@@ -301,3 +303,52 @@ def test_moved_analysis_keeps_saved_store(client, tmp_path, monkeypatch) -> None
|
||||
assert len(keyed) == analyze.KEEP_PER_YEAR + 1
|
||||
assert analyze.segment_path(tmp_path, client.segments[0]).name in keyed
|
||||
assert client.get(url).json() == after
|
||||
|
||||
|
||||
def test_terrain_extent_uses_whole_ends(client) -> None:
|
||||
got = client.get(f"/api/projects/{PID}/plan/terrain/extent?margin_m=400").json()
|
||||
m = 400.0
|
||||
assert got["margin_m"] == m and got["have"] is None and got["covered"] is False
|
||||
# 전체 시점 = 첫 연도 시점 · 전체 종점 = 끝 연도 종점(연도 구간 아님)
|
||||
assert got["box_m"] == [P0[0] - m, P0[1] - m, P2[0] + m, P2[1] + m]
|
||||
extra = client.get(
|
||||
f"/api/projects/{PID}/plan/terrain/extent", params={"extra": json.dumps([[2000, 0]])}
|
||||
).json()
|
||||
assert extra["box_m"][2] == 2000 + extra["margin_m"]
|
||||
bad = client.get(f"/api/projects/{PID}/plan/terrain/extent", params={"extra": "[1]"})
|
||||
assert bad.status_code == 400
|
||||
|
||||
|
||||
def test_terrain_post_runs_one_set(client, monkeypatch) -> None:
|
||||
import asyncio
|
||||
|
||||
calls: list = []
|
||||
saved: list = []
|
||||
works: list = []
|
||||
|
||||
def ensure(root, points, epsg, margin_m, report):
|
||||
calls.append((points, epsg, margin_m))
|
||||
return {"sheets": {"downloaded": ["1"]}, "models": [{"model_file_path": "m"}]}
|
||||
|
||||
async def save(project_id, models, epsg):
|
||||
saved.append((models, epsg))
|
||||
|
||||
def start(project_id, kind, work):
|
||||
works.append(work)
|
||||
return {"status": "accepted", "kind": kind}
|
||||
|
||||
def post(body):
|
||||
res = client.post(f"/api/projects/{PID}/plan/terrain", json=body)
|
||||
asyncio.run(works.pop()(lambda percent, message: None))
|
||||
return res
|
||||
|
||||
monkeypatch.setattr(router_mod.terrain, "ensure_plan_terrain", ensure)
|
||||
monkeypatch.setattr(router_mod, "_save_models", save)
|
||||
monkeypatch.setattr(router_mod, "_start_job", start)
|
||||
res = post({"margin_m": 500, "extra": [[5, 6]]})
|
||||
assert res.json() == {"status": "accepted", "kind": "terrain"}
|
||||
assert calls == [([P0, P2, (5.0, 6.0)], 5186, 500.0)]
|
||||
assert saved == [([{"model_file_path": "m"}], 5186)]
|
||||
# 옛 화면 꼴(points 만 · 빈 몸) — 여유 처음 값 · 전체 시점 · 종점
|
||||
post({"points": []})
|
||||
assert calls[-1] == ([P0, P2], 5186, None)
|
||||
|
||||
Reference in New Issue
Block a user