diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_Api.ts b/D03_PlanAnalysis/D03_PlanAnalysis_Api.ts index 2228fe371..6ebd7774b 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_Api.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_Api.ts @@ -63,8 +63,9 @@ export interface TerrainExtent { covered: boolean; sheets: Array<{ no: string; ring: Array<[number, number]>; stored: boolean }>; cells: number; - /** 받기 칸 상한(넘으면 받기 413). */ - max_cells: number; + /** 공용 창고 이어 붙일 도엽 수 · 한 번에 상한(넘으면 받기 413). */ + store_sheets: number; + max_sheets: number; } /** 그은 선 서버 꼴(562-2) — 마크 = 조각 경계 · 조각 없는 구역 = 연도 없음. */ @@ -130,7 +131,7 @@ export const fetchTerrainExtent = ( return request(`${base(projectId)}/terrain/extent?${query}`); }; -/** 받기 — 칸 상한 넘음 = 오류(413 글). */ +/** 받기 — 도엽 상한 넘음 = 오류(413 글). */ export const startTerrain = ( projectId: string, marginM: number | null, diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_Router.py b/D03_PlanAnalysis/D03_PlanAnalysis_Router.py index 8d7386481..f303d8f13 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_Router.py +++ b/D03_PlanAnalysis/D03_PlanAnalysis_Router.py @@ -14,7 +14,7 @@ (PLAN 591-2 · 기준점 = 처음 받는 기준일 뿐). 위치 바꿈(PLAN 595 · 꼴 = PLAN 595 절) = 몸 `center`(위치 칸 캐시 기준점) 둘레를 더함 · 받은 범위에서 `FAR_M` 넘음 = 「far」 물음 · 다른 TM 띠 = 「band」 물음 → `reset` = 옛 범위 버리고 -center 둘레만 · 칸 `MAX_CELLS` 넘음 = 413 · 진행은 `models_plan/plan_job_<일>.json` 에도 적어 +center 둘레만 · 창고 도엽 `MAX_SHEETS` 넘음 = 413 · 진행은 `models_plan/plan_job_<일>.json` 에도 적어 서버가 다시 켜지면 「끊김」(interrupted). 노선 정본(손 그은 선)은 `D03_PlanAnalysis_Network`(PLAN 562-2 · 옛 추천 엔진 · 꺾임점 R 걷음). @@ -221,7 +221,7 @@ async def plan_terrain_extent( @router.post("/{project_id}/plan/terrain", response_model=None) async def plan_terrain(project_id: UUID, body: TerrainBody | None = None) -> dict[str, Any]: """사업 전체 수치지형도 · 위성 · DEM 받기(뒤에서) — 진행은 `GET …/plan/progress` · - 위치 칸 가운데가 멀거나 다른 띠 = 물음(`far` · `band`) · 칸 상한 넘음 = 413.""" + 위치 칸 가운데가 멀거나 다른 띠 = 물음(`far` · `band`) · 도엽 상한 넘음 = 413.""" body = body or TerrainBody() center = (body.center.lon, body.center.lat) if body.center else None root, epsg, ends, band = await _plan_ends(project_id, center, body.reset) @@ -243,13 +243,10 @@ async def plan_terrain(project_id: UUID, body: TerrainBody | None = None) -> dic "옛 받은 범위를 버리고 새로 받을까요?", } points = [*ends, *body.extra] - cells = terrain.cells_of(terrain.wanted_box(points, terrain.clamp_margin(body.margin_m), have)) - if cells > terrain.MAX_CELLS: - raise HTTPException( - status_code=413, - detail=f"받을 범위가 너무 큽니다 — 칸 {cells / 1e4:.0f}만 · " - f"상한 {terrain.MAX_CELLS / 1e4:.0f}만(지점 · 선을 모으거나 위치를 옮겨 새로 받으세요)", - ) + want = terrain.wanted_box(points, terrain.clamp_margin(body.margin_m), have) + count = terrain.store_sheet_count(want, epsg) + if count > terrain.MAX_SHEETS: + raise HTTPException(status_code=413, detail=terrain.sheet_limit_message(count)) async def work(report: Any) -> None: got = await asyncio.to_thread( diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_Terrain.py b/D03_PlanAnalysis/D03_PlanAnalysis_Terrain.py index b0b78904c..1221d9bc7 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_Terrain.py +++ b/D03_PlanAnalysis/D03_PlanAnalysis_Terrain.py @@ -5,7 +5,7 @@ - 범위 = 전체 시점 · 종점(+ 그은 점) 상자 + 여유 · 받은 범위가 있으면 그 범위와 합침(줄지 않음) · 새로(`reset` · 위치를 멀리 옮김) = 옛 받은 범위 · 병합 도엽 · 위성 범위를 버리고 새 상자만 · - 칸 상한 `MAX_CELLS`(넘으면 라우터가 거절) + 칸 상한 도엽 `MAX_SHEETS`(공용 창고 · 넘으면 라우터가 거절) - 도엽 = 범위 상자에 닿는 도엽 모두 → `ensure_sheets`(없는 것만 · prune 없음) - 병합 = 지난번 병합 도엽 ∪ 이번 도엽(옛 도엽 유지 · 열쇠 같으면 건너뜀) - 위성 = 범위 도엽 도곽 ∪ 옛 위성 범위(덮으면 건너뜀) @@ -65,9 +65,9 @@ DEFAULT_MARGIN_M = 350.0 MAX_MARGIN_M = 5000.0 # 처음 받는 범위 = 등록 기준점 둘레 ± 1 km(요구 453 · PLAN 591-2) SITE_RADIUS_M = 1000.0 -# 위치 바꿈이 받은 범위에서 이만큼 넘게 멀면 묻고 새로 · 받을 DEM 칸 상한(요구서_463 3장 4) +# 위치 바꿈이 받은 범위에서 이만큼 넘게 멀면 묻고 새로 · 받을 도엽 상한(요구서_463 3장 4 · 613-1) FAR_M = 5000.0 -MAX_CELLS = 5_000_000 +MAX_SHEETS = 36 # 한 번에 이어 붙이는 공용 창고 도엽 수 상한(6 × 6) # ponytail: 새 도엽 함께 셀 수 — 장당 메모리 1 GB 안팎 · 넘으면 차례로 WARM_WORKERS = 4 STAGE = Path("B04_PreProcess") @@ -125,10 +125,23 @@ def wanted_box(points: list[XY], margin_m: float, have: Box | None) -> Box: def cells_of(box: Box) -> int: - """DEM 칸 수 어림 — 받기 시간 · 메모리 가늠 · 상한 판정.""" + """DEM 칸 수 어림 — 받기 시간 · 메모리 가늠.""" return int((box[2] - box[0]) * (box[3] - box[1]) / PLAN_TERRAIN_GRID_M**2) +def store_sheet_count(box: Box, epsg: int) -> int: + """공용 창고에서 이어 붙일 도엽 수 — 상한 판정.""" + return len(terrain_store.sheets_for_box(box, epsg)) + + +def sheet_limit_message(count: int) -> str: + """도엽 상한 넘음 알림 글 — 몇 장 · 줄이는 법.""" + return ( + f"받을 범위가 너무 넓습니다 — 도엽 {count}장 · 한 번에 {MAX_SHEETS}장까지" + "(지점 · 선을 모으거나 위치를 옮겨 새로 받으세요)" + ) + + def distance_to_box(xy: XY, box: Box) -> float: """점 ~ 상자 거리(m · 안이면 0).""" dx = max(box[0] - xy[0], 0.0, xy[0] - box[2]) @@ -195,7 +208,8 @@ def plan_extent( "covered": covers(have, box), "sheets": sheets, "cells": cells_of(box), - "max_cells": MAX_CELLS, + "store_sheets": store_sheet_count(box, epsg), + "max_sheets": MAX_SHEETS, } diff --git a/resources/tester/test_530_1_plan_terrain.py b/resources/tester/test_530_1_plan_terrain.py index aa877ba7b..44593681b 100644 --- a/resources/tester/test_530_1_plan_terrain.py +++ b/resources/tester/test_530_1_plan_terrain.py @@ -157,8 +157,28 @@ def test_cells_distance_and_limit(fake) -> None: 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 + assert terrain.MAX_SHEETS == 36 and terrain.FAR_M == 5000 + view = terrain.plan_extent(root, [A, B], EPSG) + assert view["max_sheets"] == terrain.MAX_SHEETS and "max_cells" not in view + assert view["store_sheets"] == terrain.store_sheet_count(tuple(view["box_m"]), EPSG) + + +def test_sheet_limit_counts_store_sheets(fake) -> None: + """613-1 — 상한 = 공용 창고 도엽 수(칸 수 아님) · 36 장까지 · 넘으면 알림 글에 장 수.""" + root, _ = fake + small = terrain.plan_extent(root, [A, B], EPSG) + assert 1 <= small["store_sheets"] <= 4 and small["cells"] < 2_000_000 + # 남쪽 1.5 km 늘림 — 옛 칸 상한(500 만 칸)은 넘지만 도엽은 상한 안 + south = (A[0], A[1] - 6000.0) + wide = terrain.plan_extent(root, [A, B, south], EPSG) + assert wide["store_sheets"] <= terrain.MAX_SHEETS + # 37 장 이상 — 약 6 도엽 폭 넘게 벌림 + huge = terrain.plan_extent(root, [A, (A[0] + 20000.0, A[1] + 22000.0)], EPSG) + assert huge["store_sheets"] > terrain.MAX_SHEETS + text = terrain.sheet_limit_message(huge["store_sheets"]) + assert ( + f"도엽 {huge['store_sheets']}장" in text and "36장까지" in text and "새로 받으세요" in text + ) def test_extent_no_download_and_covered(fake) -> None: diff --git a/resources/tester/test_530_2_plan_router.py b/resources/tester/test_530_2_plan_router.py index 10bfe6970..522ce8364 100644 --- a/resources/tester/test_530_2_plan_router.py +++ b/resources/tester/test_530_2_plan_router.py @@ -227,13 +227,16 @@ def test_center_other_band_asks_then_moves(posting) -> None: 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 만 칸 +def test_sheet_limit_413(posting) -> None: + """613-1 — 6.7 × 6.7 km(옛 칸 상한 넘음)는 받아짐 · 도엽 37 장 이상은 413 + 장 수 알림.""" + posting.plan["points"].append({"x": OX + 6000.0, "y": OY + 6000.0}) + assert posting.run_post({}).status_code == 200 + assert len(posting.calls) == 1 + posting.plan["points"].append({"x": OX + 22000.0, "y": OY + 24000.0}) res = posting.run_post({}) - assert res.status_code == 413 and "상한 500만" in res.json()["detail"] - assert posting.calls == [] + detail = res.json()["detail"] + assert res.status_code == 413 and "한 번에 36장까지" in detail and "도엽 " in detail + assert len(posting.calls) == 1 def test_progress_interrupted_after_restart(client, monkeypatch) -> None: