# -*- coding: utf-8 -*- """589-2 B04 · B05 GL 바탕 — MapLibre 카메라가 TM 주인 화면(589-1 자리)을 따라감. - 카메라(화면 가운데 1차 맞춤 · 닮음꼴) + 칸 CSS 행렬(남는 늘림) → MapLibre 식 화면 px = 캔버스 자리 (격자 쌍선형 · 화면 식) 0.5 px 안(처음 화면 · 노선 자리 · 크게 당김 · 돌림 30° · 화면 밖 반 폭까지) - 칸 행렬 = 늘이기만(고윳값 ≥ 1 · 웹 메르카토르 가로세로 차 0.4 %) - TM 겹 되돌림(정규화 → 경위도) — 다시 정규화하면 1 mm 안 · pyproj 와 2 cm 안 - 준비한 겹 → GeoJSON — 상자 자르기(하천 = LAS 등고선 있는 데까지) · 닫힌 고리 · 점 - 유역 고리(조각 · 구멍) → 다각형 — 캔버스 짝홀 채움과 같은 묶음 Node 헬퍼(`helper_589_2_gl_frame.cjs`)가 진짜 TS 를 돌린다. """ import json import math import os import subprocess import pytest from pyproj import Transformer from B04_PreProcess.B04_PreProcess_Engine_VWorld import tm_grid_for_meta from resources.tester.test_589_tm_grid import SITES, _meta HERE = os.path.dirname(os.path.abspath(__file__)) # (w, h, 배율, 옮김 x, y, 돌림 도) — 처음 화면 · 노선 자리 · 크게 당김 · 돌림 VIEWS = [ (1400, 560, 1, 0, 0, 0), (1200, 700, 2.33, -120, 80, 0), (1400, 560, 300, -60000, 40000, 0), (1200, 700, 4, 200, -150, 30), ] LAYER = { "features": [ { "kind": "line", "labelValue": 125, "parts": [{"coords": [0.0, 0.5, 0.5, 0.5, 1.0, 0.5], "closed": False}], }, { "kind": "line", "labelValue": None, "parts": [{"coords": [0.2, 0.2, 0.4, 0.2, 0.4, 0.4], "closed": True}], }, {"kind": "point", "labelValue": None, "parts": [{"coords": [0.3, 0.3, 0.9, 0.9]}]}, ] } BOX = [0.25, 0.0, 0.75, 1.0] RINGS = [ [[0, 0], [10, 0], [10, 10], [0, 10]], # 바깥 [[2, 2], [8, 2], [8, 8], [2, 8]], # 구멍 [[4, 4], [6, 4], [6, 6], [4, 6]], # 구멍 속 섬 [[20, 0], [25, 0], [25, 5]], # 떨어진 조각 ] def _run(site): crs, box, _ = SITES[site] meta = _meta(crs, box) meta["tm_grid"] = tm_grid_for_meta(meta, crs) lon_min, lon_max, lat_min, lat_max = box # 상자 안 · 둘레 반 폭(화면 밖까지) lonlat = [ (lon_min + a / 8 * (lon_max - lon_min), lat_min + b / 8 * (lat_max - lat_min)) for a in range(-4, 13) for b in range(-4, 13) ] norm = [(a / 6, b / 6) for a in range(-1, 8) for b in range(-1, 8)] payload = { "meta": meta, "lonlat": lonlat, "views": VIEWS, "norm": norm, "layer": LAYER, "box": BOX, "rings": RINGS, } proc = subprocess.run( ["node", os.path.join(HERE, "helper_589_2_gl_frame.cjs")], input=json.dumps(payload), capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return meta, norm, json.loads(proc.stdout) @pytest.fixture(scope="module", params=["B", "D"]) def site(request): return request.param, *_run(request.param) def test_GL_카메라는_캔버스_자리를_1px_안에(site): name, _, _, out = site for (w, h, scale, *_rest), view in zip(VIEWS, out["views"]): worst = 0.0 for canvas, gl in view["points"]: # 화면 안 · 둘레 반 폭까지만 잰다(그 밖은 안 보임) if not (-w / 2 <= canvas[0] <= w * 1.5 and -h / 2 <= canvas[1] <= h * 1.5): continue worst = max(worst, math.hypot(canvas[0] - gl[0], canvas[1] - gl[1])) assert worst < 0.5, (name, scale, worst) # 화면 격자(귀퉁이 · 가장자리 · 가운데) — 되돌림 거쳐 제자리 corner = max(math.hypot(s[0] - g[0], s[1] - g[1]) for s, g in view["screen"]) assert corner < 0.5, (name, scale, corner) def test_남는_늘림은_칸_행렬로_늘이기만(site): _, _, _, out = site for view in out["views"]: a, b, c, d = view["camera"]["css"] # 대칭 · 고윳값 ≥ 1 · 웹 메르카토르 가로세로 차(0.4 %) 안팎 assert abs(b - c) < 1e-9 trace, det = a + d, a * d - b * c least = trace / 2 - math.sqrt(max(trace * trace / 4 - det, 0)) most = trace / 2 + math.sqrt(max(trace * trace / 4 - det, 0)) assert least == pytest.approx(1, abs=1e-9) and 1.003 < most < 1.006 def test_돌림은_카메라_도에_그대로(): _, _, out = _run("D") plain = out["views"][1]["camera"]["bearing"] turned = out["views"][3]["camera"]["bearing"] # 화면 30° 돌림 = 카메라 −30° (+ 수렴각 몫은 같음) assert abs((turned - plain) + 30) < 0.01 def test_TM_되돌림은_다시_정규화하면_제자리(site): name, meta, norm, out = site inv = Transformer.from_crs(SITES[name][0], "EPSG:4326", always_xy=True) for (nx, ny), (lon, lat, bx, by) in zip(norm, out["back"]): meters = math.hypot((bx - nx) * meta["width_meters"], (by - ny) * meta["height_meters"]) assert meters < 1e-3, (nx, ny, meters) x = meta["x_min"] + nx * meta["width_meters"] y = meta["y_min"] + (1 - ny) * meta["height_meters"] want = inv.transform(x, y) fwd = Transformer.from_crs("EPSG:4326", SITES[name][0], always_xy=True) gx, gy = fwd.transform(lon, lat) assert math.hypot(gx - x, gy - y) < 0.02, (nx, ny, want) def test_준비한_겹_GeoJSON_자르기(): _, _, out = _run("B") lines = [f for f in out["geo"]["features"] if f["geometry"]["type"] == "LineString"] # 가로줄 → 상자 0.25~0.75 만 · 높이 그대로 first = lines[0] assert first["properties"] == {"level": 125} xs = [p[0] for p in first["geometry"]["coordinates"]] assert min(xs) == pytest.approx(0.25) and max(xs) == pytest.approx(0.75) # 닫힌 고리 → 상자 안 조각만(0.25 왼쪽 몫 잘림) ring = lines[1]["geometry"]["coordinates"] assert all(p[0] >= 0.25 - 1e-12 for p in ring) # 점 — 상자 밖(0.9) 뺌 points = [f for f in out["geo"]["features"] if f["geometry"]["type"] == "MultiPoint"] assert points[0]["geometry"]["coordinates"] == [[0.3, 0.3]] # 상자 없으면 통째 · 닫힌 고리는 첫 점으로 닫음 closed = out["geoAll"]["features"][1]["geometry"]["coordinates"] assert closed[0] == closed[-1] and len(closed) == 4 def test_유역_고리는_짝홀_다각형(): _, _, out = _run("B") polygons = out["polygons"] # 바깥(+ 구멍) · 구멍 속 섬 · 떨어진 조각 = 셋 assert len(polygons) == 3 assert len(polygons[0]) == 2 and polygons[0][1][0] == [2, 2] assert len(polygons[1]) == 1 and polygons[1][0][0] == [4, 4] assert len(polygons[2]) == 1 and polygons[2][0][0] == [20, 0]