# -*- coding: utf-8 -*- """509-1 평면도(B05 노선 편집)용 도엽 — 노선 둘레 띠로 잘라 둠(서버 · 파생 저장본). - 범위 = 저장 노선 bbox + 띠 300 + 여유 300 m(50 m 눈금 바깥 맞춤) · `clip_lonlat` 붙음 - 안에 다 든 가닥 그대로 · 밖 가닥 빠짐 · 걸친 가닥은 범위 안 꼭짓점만 - 같은 입력 = 같은 저장본(다시 안 셈) · 사본이 바뀌면 다시 셈 - 노선 없음 · 모르는 범위 = 사본 그대로 - 원본 · 사본 파일은 안 바뀜 · 저장본은 `_derived/sheet_route/` 에만 - 화면 쪽 `sheetCoversBand` — 띠가 범위 밖이면 통째 사본을 받음 · 상수 짝(띠 300 · 줌아웃 2 배) 임시 폴더에 사업지(1 m = 0.00001 도)를 지어 시험 — 공유 storage · DB 안 씀. """ import json import os import re import subprocess from pathlib import Path import pytest from B04_PreProcess import B04_PreProcess_Engine_SheetClip as clip from common_util.common_util_route_geometry import planned_route_working_path, write_route_csv HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] META = { "x_min": 0, "y_min": 0, "width_meters": 10000, "height_meters": 10000, "lon_min": 127.0, "lon_max": 127.1, "lat_min": 37.0, "lat_max": 37.1, } def lonlat(x: float, y: float) -> list[float]: return [127.0 + x * 1e-5, 37.0 + y * 1e-5] def line_feature(points: list[tuple[float, float]], level: int) -> dict: return { "type": "Feature", "properties": {"등고수치": level}, "geometry": {"type": "LineString", "coordinates": [lonlat(x, y) for x, y in points]}, } @pytest.fixture() def project(tmp_path: Path) -> tuple[Path, Path]: processed = tmp_path / "B04_PreProcess" / "processed" processed.mkdir(parents=True) (processed / "vworld_satellite_meta.json").write_text(json.dumps(META), encoding="utf-8") write_route_csv( planned_route_working_path(tmp_path), [{"x": 5000, "y": 5000}, {"x": 5500, "y": 5200}] ) source = processed / "도엽_등고선.display.geojson" source.write_text( json.dumps( { "type": "FeatureCollection", "features": [ line_feature([(5100, 5100), (5200, 5150), (5300, 5100)], 100), # 안 line_feature([(9000, 9000), (9500, 9100)], 105), # 밖 line_feature([(x, 5100) for x in range(0, 10001, 100)], 110), # 걸침 ], } ), encoding="utf-8", ) return tmp_path, source def test_범위는_노선_bbox_더하기_600m(project): root, _ = project assert clip.clip_rect_m(root, "route", META) == (4400, 4400, 6100, 5800) assert clip.ROUTE_BAND_M + clip.ROUTE_SPARE_M == 600 def test_자르기(project): root, source = project before = source.read_bytes() out = clip.clipped_sheet(root, source, "route") assert out.parent == root / "_derived" / "sheet_route" data = json.loads(out.read_text(encoding="utf-8")) assert data["clip_lonlat"] == pytest.approx([127.044, 37.044, 127.061, 37.058]) levels = [f["properties"]["등고수치"] for f in data["features"]] assert levels == [100, 110] inside = data["features"][0]["geometry"] assert inside["coordinates"] == [lonlat(5100, 5100), lonlat(5200, 5150), lonlat(5300, 5100)] crossing = data["features"][1]["geometry"]["coordinates"] assert 15 <= len(crossing) <= 20 # 101 꼭짓점 → 범위 안(4400~6100)만 + 두 끝 assert all(127.044 - 1e-9 <= lon <= 127.061 + 1e-9 for lon, _ in crossing) assert source.read_bytes() == before def test_같은_입력은_다시_안_셈(project): root, source = project first = clip.clipped_sheet(root, source, "route") stamp = first.stat().st_mtime_ns again = clip.clipped_sheet(root, source, "route") assert again == first and again.stat().st_mtime_ns == stamp def test_사본이_바뀌면_다시_셈(project): root, source = project first = clip.clipped_sheet(root, source, "route") data = json.loads(source.read_text(encoding="utf-8")) data["features"] = data["features"][:1] source.write_text(json.dumps(data), encoding="utf-8") os.utime(source, ns=(source.stat().st_atime_ns, source.stat().st_mtime_ns + 10**9)) second = clip.clipped_sheet(root, source, "route") assert second != first assert len(json.loads(second.read_text(encoding="utf-8"))["features"]) == 1 def test_노선을_조금_옮기면_같은_열쇠(project): root, source = project first = clip.clipped_sheet(root, source, "route") write_route_csv( planned_route_working_path(root), [{"x": 5010, "y": 5010}, {"x": 5490, "y": 5190}] ) assert clip.clipped_sheet(root, source, "route") == first def test_범위를_못_정하면_사본_그대로(project): root, source = project assert clip.clipped_sheet(root, source, "nope") == source planned_route_working_path(root).unlink() assert clip.clipped_sheet(root, source, "route") == source assert not (root / "_derived" / "sheet_route").exists() def test_화면_띠_덮음_판정(): proc = subprocess.run( ["node", str(HERE / "helper_509_1_sheet_covers.cjs")], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr r = json.loads(proc.stdout) assert r["saved"] and r["within"] assert not r["beyondEast"] and not r["beyondSouth"] assert r["noClip"] and r["noSheet"] and r["noLine"] def test_상수_짝(): render = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Render.ts").read_text("utf-8") zoom = (ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_ZoomRange.ts").read_text("utf-8") band = re.search(r"export const CONTOUR_BAND_M = (\d+)", render) factor = re.search(r"export const ZOOM_OUT_ROUTE_FACTOR = (\d+)", zoom) assert band and float(band.group(1)) == clip.ROUTE_BAND_M assert factor and float(factor.group(1)) == clip.ZOOM_OUT_ROUTE_FACTOR