Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1
- 충돌 D03_PlanAnalysis_Api.ts: 지형 받기 꼴 이름(TerrainCenter · TerrainAim) 같은 뜻 둘 → sub2(595 몫) TerrainAim 쪽 살림 · 쓰는 곳 없음 - PlanForestRoads bounds · distance_m 두 벌 → sub2 한 벌 · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""D 예정노선 지형 받기(PLAN 530-1 · 561-2) — 사업 전체 한 벌 · 범위 보기 = 내려받기 없음 ·
|
||||
옛 도엽 유지 · 새 도엽만 · 범위는 줄지 않음 · 빠짐 없음.
|
||||
"""D 예정노선 지형 받기(PLAN 530-1 · 561-2 · 595) — 사업 전체 한 벌 · 범위 보기 = 내려받기 없음 ·
|
||||
옛 도엽 유지 · 새 도엽만 · 범위는 줄지 않음 · 빠짐 없음 · 지적도 뺌 · 새로(reset) = 옛 범위 버림 ·
|
||||
받은 범위 좌표계 · 칸 상한 · 거리.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_530_1_plan_terrain.py -q
|
||||
"""
|
||||
@@ -54,7 +55,6 @@ def fake(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(terrain, "parse_and_merge_sheets", merge)
|
||||
monkeypatch.setattr(terrain, "download_vworld_satellite_map", satellite)
|
||||
monkeypatch.setattr(terrain, "build_sheet_surface_model", dem)
|
||||
monkeypatch.setattr(terrain.layers, "ensure_cadastral", lambda *a: {"count": 0}) # 569-1
|
||||
return tmp_path, seen
|
||||
|
||||
|
||||
@@ -104,6 +104,39 @@ def test_one_set_and_extend_keeps_old(fake) -> None:
|
||||
assert len(seen["merge"]) == merges and third["box_m"] == grown
|
||||
|
||||
|
||||
def test_reset_drops_old_box_sheets_satellite(fake) -> None:
|
||||
"""595-2 — 새로 = 옛 받은 범위 · 병합 도엽 · 위성 범위 버림 · 좌표계 · 가운데 기록 ·
|
||||
다른 좌표계로 물으면 받은 범위 없음 · 지적도는 지형 일에 없음."""
|
||||
root, seen = fake
|
||||
terrain.ensure_plan_terrain(root, [A, B], EPSG, center=(127.5, 36.0))
|
||||
old = set(seen["merge"][-1])
|
||||
far = [(A[0] + 40000.0, A[1]), (A[0] + 41000.0, A[1] + 1000.0)]
|
||||
got = terrain.ensure_plan_terrain(root, far, EPSG, reset=True, center=(128.0, 36.0))
|
||||
m = terrain.DEFAULT_MARGIN_M
|
||||
assert got["box_m"] == pytest.approx(
|
||||
[far[0][0] - m, far[0][1] - m, far[1][0] + m, far[1][1] + m]
|
||||
)
|
||||
assert not old & set(seen["merge"][-1]) # 옛 도엽 병합에서 빠짐(zip 은 남음)
|
||||
assert seen["satellite"][-1]["x"][0] > A[0] + 30000 # 옛 위성 범위와 안 합침
|
||||
assert "cadastral" not in got
|
||||
meta = terrain.received_meta(root)
|
||||
assert meta["epsg"] == EPSG and meta["center"] == [128.0, 36.0]
|
||||
assert terrain.received_box(root, EPSG) == pytest.approx(tuple(got["box_m"]))
|
||||
assert terrain.received_box(root, 5187) is None # 다른 띠 = 받은 범위 아님
|
||||
# 새로 아닌 이어 받기 = 가운데 기록 그대로
|
||||
terrain.ensure_plan_terrain(root, far, EPSG)
|
||||
assert terrain.received_meta(root)["center"] == [128.0, 36.0]
|
||||
|
||||
|
||||
def test_cells_distance_and_limit(fake) -> None:
|
||||
root, _ = fake
|
||||
assert terrain.cells_of((0.0, 0.0, 2000.0, 2000.0)) == 1_000_000 # 2 m 격자
|
||||
assert terrain.distance_to_box((5.0, 5.0), (0.0, 0.0, 10.0, 10.0)) == 0
|
||||
assert terrain.distance_to_box((13.0, 14.0), (0.0, 0.0, 10.0, 10.0)) == 5
|
||||
assert terrain.MAX_CELLS == 5_000_000 and terrain.FAR_M == 5000
|
||||
assert terrain.plan_extent(root, [A, B], EPSG)["max_cells"] == terrain.MAX_CELLS
|
||||
|
||||
|
||||
def test_extent_no_download_and_covered(fake) -> None:
|
||||
root, seen = fake
|
||||
before = terrain.plan_extent(root, [A, B], EPSG)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""D 지형 API(PLAN 530-1 · 561-2 · 591-2) — 범위 = 지점(설계 내용 파일) 상자 + 여유 · 지점 · 선이
|
||||
없으면 등록 기준점 둘레 ± 1 km · 받기 = 한 벌.
|
||||
"""D 지형 API(PLAN 530-1 · 561-2 · 591-2 · 595) — 범위 = 지점(설계 내용 파일) 상자 + 여유 · 지점 ·
|
||||
선이 없으면 등록 기준점 둘레 ± 1 km · 받기 = 한 벌 · 위치 칸 가운데 = 둘레 덧 · 멀다 · 다른 띠
|
||||
물음 · 새로(reset) · 칸 상한 413 · 서버 다시 켜짐 = 끊김.
|
||||
|
||||
옛 분석 API 시험(GET · PUT analysis · 꺾임점 고침)은 옛 추천 엔진과 함께 걷음(PLAN 562-2) ·
|
||||
노선 정본 시험 = `test_530_2_plan_analyze.py` · 지형 셈 시험 = `test_530_1_plan_terrain.py`.
|
||||
@@ -20,6 +21,8 @@ from D03_PlanAnalysis import D03_PlanAnalysis_Router as router_mod
|
||||
PID = "12953f5a-5ba1-42ad-a427-8ac857a58bc2"
|
||||
P0, P2 = (0.0, 0.0), (900.0, 400.0)
|
||||
URL = f"/api/projects/{PID}/plan/terrain"
|
||||
OX, OY = 200000.0, 500000.0 # 595 시험 = 중부 띠 안(경도 127°)
|
||||
Q0, Q2 = (OX + P0[0], OY + P0[1]), (OX + P2[0], OY + P2[1])
|
||||
|
||||
|
||||
class _Pool:
|
||||
@@ -51,6 +54,7 @@ def client(tmp_path, monkeypatch):
|
||||
test = TestClient(app)
|
||||
test.plan = plan
|
||||
test.info = info
|
||||
test.root = tmp_path
|
||||
return test
|
||||
|
||||
|
||||
@@ -90,14 +94,14 @@ def test_post_runs_one_set(client, monkeypatch) -> None:
|
||||
saved: list = []
|
||||
works: list = []
|
||||
|
||||
def ensure(root, points, epsg, margin_m, report):
|
||||
def ensure(root, points, epsg, margin_m, report, **kw):
|
||||
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):
|
||||
def start(project_id, kind, work, root=None):
|
||||
works.append(work)
|
||||
return {"status": "accepted", "kind": kind}
|
||||
|
||||
@@ -116,3 +120,154 @@ def test_post_runs_one_set(client, monkeypatch) -> None:
|
||||
# 옛 화면 꼴(points 몸) — 여유 처음 값 · 전체 시점 · 종점
|
||||
post({"points": []})
|
||||
assert calls[-1] == ([P0, P2], 5186, None)
|
||||
|
||||
|
||||
# ── 595 위치 바꿈 · 상한 · 끊김 ──
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def posting(client, monkeypatch):
|
||||
"""POST 받기 — 일은 바로 돌림 · ensure 부름(점 · 좌표계 · reset · center) 기록."""
|
||||
calls: list = []
|
||||
|
||||
def ensure(root, points, epsg, margin_m, report, reset=False, center=None):
|
||||
calls.append({"points": points, "epsg": epsg, "reset": reset, "center": center})
|
||||
return {"sheets": {"downloaded": []}, "models": []}
|
||||
|
||||
async def save(project_id, models, epsg):
|
||||
return None
|
||||
|
||||
works: list = []
|
||||
|
||||
def start(project_id, kind, work, root=None):
|
||||
works.append(work)
|
||||
return {"status": "accepted", "kind": kind}
|
||||
|
||||
def post(body):
|
||||
res = client.post(URL, json=body)
|
||||
while works: # 일은 응답 뒤 돌림(앱 고리 밖)
|
||||
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)
|
||||
client.plan["points"] = [{"x": Q0[0], "y": Q0[1]}, {"x": Q2[0], "y": Q2[1]}]
|
||||
client.calls = calls
|
||||
client.run_post = post
|
||||
return client
|
||||
|
||||
|
||||
def _have(client, box, epsg=5186, center=None):
|
||||
meta = {"box": list(box), "margin_m": 350.0, "epsg": epsg}
|
||||
if center:
|
||||
meta["center"] = list(center)
|
||||
folder = router_mod.terrain.models_dir_of(client.root)
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
(folder / router_mod.terrain.TERRAIN_META).write_text(json.dumps(meta), "utf-8")
|
||||
|
||||
|
||||
def test_center_near_adds_site_ring(posting) -> None:
|
||||
"""가까운 위치 바꿈 = 지점 + 새 가운데 둘레 · 받은 범위와 합침."""
|
||||
from common_util.common_util_crs import lonlat_to_tm, tm_to_lonlat
|
||||
|
||||
_have(posting, (OX - 500.0, OY - 500.0, OX + 1500.0, OY + 1000.0))
|
||||
lon, lat = tm_to_lonlat(OX + 3000.0, OY, 5186) # 받은 범위에서 1.5 km
|
||||
res = posting.run_post({"center": {"lon": lon, "lat": lat}}).json()
|
||||
assert res["status"] == "accepted"
|
||||
call = posting.calls[-1]
|
||||
x, y = lonlat_to_tm(lon, lat, 5186)
|
||||
r = router_mod.terrain.SITE_RADIUS_M - router_mod.terrain.DEFAULT_MARGIN_M
|
||||
assert call["points"][:2] == [Q0, Q2] and call["points"][2] == pytest.approx((x - r, y - r))
|
||||
assert call["reset"] is False and call["center"] == (lon, lat)
|
||||
# extent 에도 캐시 가운데 = 둘레가 상자에 들어감
|
||||
got = posting.get(f"{URL}/extent", params={"lon": lon, "lat": lat}).json()
|
||||
assert got["box_m"][2] == pytest.approx(x + router_mod.terrain.SITE_RADIUS_M)
|
||||
|
||||
|
||||
def test_center_far_asks_then_reset(posting) -> None:
|
||||
"""5 km 넘음 = 「far」(받지 않음) · reset = 옛 범위 버리고 새 ±1 km(지점 안 넣음)."""
|
||||
from common_util.common_util_crs import tm_to_lonlat
|
||||
|
||||
_have(posting, (OX - 500.0, OY - 500.0, OX + 1500.0, OY + 1000.0))
|
||||
lon, lat = tm_to_lonlat(OX + 8000.0, OY, 5186) # 6.5 km
|
||||
body = {"center": {"lon": lon, "lat": lat}}
|
||||
far = posting.run_post(body).json()
|
||||
assert far["status"] == "far" and far["distance_m"] == pytest.approx(6500, abs=2)
|
||||
assert "km" in far["message"] and posting.calls == []
|
||||
res = posting.run_post({**body, "reset": True}).json()
|
||||
assert res["status"] == "accepted"
|
||||
call = posting.calls[-1]
|
||||
assert call["reset"] is True and len(call["points"]) == 2 # 둘레 두 귀만
|
||||
assert call["points"][0][0] == pytest.approx(OX + 8000 - 650, abs=1)
|
||||
|
||||
|
||||
def test_center_other_band_asks_then_moves(posting) -> None:
|
||||
"""다른 TM 띠 = 「band」 · 확인(reset) 뒤 그 띠 · 그 뒤엔 받은 지형 띠를 따름."""
|
||||
posting.plan["lines"] = [{"id": "a"}]
|
||||
body = {"center": {"lon": 129.1017, "lat": 36.8128}} # 용화(동부 5187)
|
||||
ask = posting.run_post(body).json()
|
||||
assert ask == {
|
||||
"status": "band",
|
||||
"epsg": 5186,
|
||||
"center_epsg": 5187,
|
||||
"message": ask["message"],
|
||||
}
|
||||
assert posting.calls == []
|
||||
# 확인 전 extent = 가운데 무시(옛 띠 그대로)
|
||||
assert posting.get(f"{URL}/extent", params={"lon": 129.1, "lat": 36.8}).json()["epsg"] == 5186
|
||||
posting.run_post({**body, "reset": True})
|
||||
call = posting.calls[-1]
|
||||
assert call["epsg"] == 5187 and call["reset"] is True and len(call["points"]) == 2
|
||||
# 받은 뒤([저장] 전) — 가운데 없이 물어도 받은 지형 띠 · 가운데 둘레
|
||||
(x0, y0), (x1, y1) = call["points"]
|
||||
box = [x0 - 400, y0 - 400, x1 + 400, y1 + 400]
|
||||
_have(posting, box, epsg=5187, center=(129.1017, 36.8128))
|
||||
got = posting.get(f"{URL}/extent").json()
|
||||
assert got["epsg"] == 5187 and got["covered"] is True
|
||||
|
||||
|
||||
def test_cells_limit_413(posting) -> None:
|
||||
posting.plan["points"].append(
|
||||
{"x": OX + 6000.0, "y": OY + 6000.0}
|
||||
) # 6.7 × 6.7 km = 1,100 만 칸
|
||||
res = posting.run_post({})
|
||||
assert res.status_code == 413 and "상한 500만" in res.json()["detail"]
|
||||
assert posting.calls == []
|
||||
|
||||
|
||||
def test_progress_interrupted_after_restart(client, monkeypatch) -> None:
|
||||
"""도는 기록이 남았는데 기억에 없음(서버 다시 켜짐) = 끊김 · 받은 적 없음 = percent 100."""
|
||||
root = client.root
|
||||
|
||||
async def tile_root(project_id):
|
||||
return root
|
||||
|
||||
monkeypatch.setattr(router_mod, "_tile_root", tile_root)
|
||||
url = f"/api/projects/{PID}/plan/progress"
|
||||
router_mod._JOBS.clear()
|
||||
idle = client.get(url).json()["terrain"]
|
||||
assert idle == {**router_mod.IDLE}
|
||||
|
||||
path = router_mod._job_path(root, "terrain")
|
||||
seen = []
|
||||
|
||||
async def work(report):
|
||||
report(65, "5/5 사업 지형면 만드는 중")
|
||||
seen.append(json.loads(path.read_text("utf-8")))
|
||||
|
||||
async def run():
|
||||
router_mod._start_job(PID, "terrain", work, root)
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
asyncio.run(run())
|
||||
assert seen[0]["running"] is True and seen[0]["percent"] == 65 # 도는 동안 기록
|
||||
assert json.loads(path.read_text("utf-8"))["running"] is False # 끝 기록
|
||||
path.write_text(json.dumps(seen[0]), "utf-8") # 도는 중 꺼짐
|
||||
router_mod._JOBS.clear() # 다시 켜짐 = 기억 빔
|
||||
got = client.get(url).json()["terrain"]
|
||||
assert got["running"] is False and got["interrupted"] is True
|
||||
assert got["percent"] == 65 and got["message"] is None and got["error"] is None
|
||||
# 끝난 기록 = 끊김 아님
|
||||
path.write_text(json.dumps({**seen[0], "running": False}), "utf-8")
|
||||
assert client.get(url).json()["terrain"] == {**router_mod.IDLE}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""D 노선분석 지도 겹(PLAN 569-1) — 도엽 겹 풀어 냄 · 지적도 1 km 조각 · 1,000 꽉 차면 넷 ·
|
||||
겹친 필지 거름 · 급경사 15° · 35° · 40° 단계 · 격자 지문 같으면 다시 안 셈.
|
||||
"""D 노선분석 지도 겹(PLAN 569-1 · 595-1) — 도엽 겹 풀어 냄 · 지적도 1 km 조각 · 1,000 꽉 차면 넷 ·
|
||||
겹친 필지 거름 · 실패 조각만 다시 · 급경사 15° · 35° · 40° 단계 · 격자 지문 같으면 다시 안 셈.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_569_1_plan_layers.py -q
|
||||
"""
|
||||
@@ -127,9 +127,29 @@ def test_cadastral_failures(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr(layers, "request_vworld_wfs", FakeWfs(fail=1))
|
||||
first = layers.ensure_cadastral(tmp_path, BOX, EPSG)
|
||||
assert first["failed"] == 1 and (tmp_path / layers.CADASTRAL_FILE).exists()
|
||||
assert (
|
||||
len(json.loads((tmp_path / layers.CADASTRAL_KEY).read_text("utf-8"))["failed_tiles"]) == 1
|
||||
)
|
||||
# 595-1 — 실패 조각만 다시 · 필지는 옛 것 + 그 조각 = 통째로 받은 것과 같음
|
||||
again = FakeWfs()
|
||||
monkeypatch.setattr(layers, "request_vworld_wfs", again)
|
||||
assert layers.ensure_cadastral(tmp_path, BOX, EPSG)["fetched"] and again.calls == 15
|
||||
retry = layers.ensure_cadastral(tmp_path, BOX, EPSG)
|
||||
assert retry["fetched"] and again.calls == 1 and retry["failed"] == 0
|
||||
assert retry["count"] == 15 * 2 + 1
|
||||
assert not layers.ensure_cadastral(tmp_path, BOX, EPSG)["fetched"] and again.calls == 1
|
||||
# 옛 열쇠(실패 조각 기록 없음) = 통째로
|
||||
key = json.loads((tmp_path / layers.CADASTRAL_KEY).read_text("utf-8"))
|
||||
key.update(failed=1)
|
||||
key.pop("failed_tiles")
|
||||
(tmp_path / layers.CADASTRAL_KEY).write_text(json.dumps(key), "utf-8")
|
||||
layers.ensure_cadastral(tmp_path, BOX, EPSG)
|
||||
assert again.calls == 1 + 15
|
||||
# 다시 받기도 모두 실패 = 옛 필지 그대로 · 실패 조각 남김(오류 아님)
|
||||
key.update(failed=1, failed_tiles=[list(layers.tiles(BOX)[0])])
|
||||
(tmp_path / layers.CADASTRAL_KEY).write_text(json.dumps(key), "utf-8")
|
||||
monkeypatch.setattr(layers, "request_vworld_wfs", FakeWfs(fail=99))
|
||||
kept = layers.ensure_cadastral(tmp_path, BOX, EPSG)
|
||||
assert kept["failed"] == 1 and kept["count"] == 15 * 2 + 1
|
||||
|
||||
|
||||
# ── 급경사 ──
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""D 노선분석 GIS 겹 더함 3(PLAN 584-3) — 올린 임도망도 SHP(zip · 낱 파일 묶음 · prj 없으면 5179 ·
|
||||
cp949 · 받은 범위로 자름 · 범위 밖 = 받지 않음 · 저장본) · 산사태취약지역 시도 표(머리 · 열 별칭 ·
|
||||
"""D 노선분석 GIS 겹 더함 3(PLAN 584-3 · 595-4) — 올린 임도망도 SHP(zip · 낱 파일 묶음 · prj 없으면
|
||||
5179 · cp949 · 기준점 ±5 km ∪ 받은 지형으로 자름 · 잘린 상자 · 기준점 거리 · 범위 밖 = 받지 않음 +
|
||||
거리 · 저장본) · 산사태취약지역 시도 표(머리 · 열 별칭 ·
|
||||
도분초 · 지번) · 점 · 지적 필지 맞춤 · 표 없는 시도 · 화면 겹 정의.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_584_3_plan_forest_road_zones.py -q
|
||||
@@ -81,7 +82,7 @@ def test_bad_uploads_keep_old(tmp_path) -> None:
|
||||
([("a.zip", b"not a zip")], "zip 파일을 열지"),
|
||||
([(n, d) for n, d in files if not n.endswith(".dbf")], ".dbf"),
|
||||
([("a.zip", _zip({"readme.txt": b"x"}))], ".shp 파일이 없습니다"),
|
||||
(_shp_files(tmp_path / "far", [LINES[2]]), "받은 범위 안에 임도가 없습니다"),
|
||||
(_shp_files(tmp_path / "far", [LINES[2]]), "받은 지형 밖뿐입니다"),
|
||||
]
|
||||
for upload, text in bad:
|
||||
with pytest.raises(ValueError, match=re.escape(text)):
|
||||
@@ -91,6 +92,26 @@ def test_bad_uploads_keep_old(tmp_path) -> None:
|
||||
assert not list((tmp_path / fr.FOLDER).glob("*.new"))
|
||||
|
||||
|
||||
def test_clip_box_site_5km_and_distance(tmp_path) -> None:
|
||||
"""595-4 — 자르는 범위 = 기준점 ±5 km ∪ 받은 지형 · 잘린 상자 · 가장 가까운 임도 거리 ·
|
||||
밖뿐 = 거절 + 거리."""
|
||||
site = (231000.0, 450600.0)
|
||||
frame = fr.clip_box(site, BOX)
|
||||
assert frame == (226000.0, 445600.0, 236000.0, 455600.0) # 5 km 상자가 받은 지형을 덮음
|
||||
assert fr.clip_box(site, (220000.0, 450000.0, 232000.0, 451000.0))[0] == 220000.0 # ∪
|
||||
assert fr.clip_box(site, None) == frame
|
||||
got = fr.save_roads(tmp_path, _shp_files(tmp_path / "a", LINES), frame, EPSG, site)
|
||||
assert (got["count"], got["total"]) == (2, 3) # 범위 밖 한 가닥(25 km)만 빠짐
|
||||
assert got["length_m"] == pytest.approx(2000 + 300, abs=1) # 받은 지형 밖 반도 들어옴
|
||||
assert got["distance_m"] == pytest.approx(100, abs=0.5) # 450600 ~ 450500
|
||||
w, s, e, n = got["bounds"]
|
||||
assert (w, s) == pytest.approx(tm_to_lonlat(229000, 450100, EPSG), abs=1e-4)
|
||||
assert (e, n) == pytest.approx(tm_to_lonlat(231500, 450500, EPSG), abs=1e-4)
|
||||
far = _shp_files(tmp_path / "far", [LINES[2]])
|
||||
with pytest.raises(ValueError, match=r"가장 가까운 임도 2[0-9]\.[0-9] km\(파일 선 1가닥\)"):
|
||||
fr.save_roads(tmp_path, far, frame, EPSG, site)
|
||||
|
||||
|
||||
def test_no_upload_is_not_found(tmp_path) -> None:
|
||||
with pytest.raises(FileNotFoundError):
|
||||
fr.road_layer(tmp_path, BOX, EPSG)
|
||||
|
||||
Reference in New Issue
Block a user