diff --git a/B04_PreProcess/B04_PreProcess_Api_Fetch.ts b/B04_PreProcess/B04_PreProcess_Api_Fetch.ts index 03f4e0599..f9d033d60 100644 --- a/B04_PreProcess/B04_PreProcess_Api_Fetch.ts +++ b/B04_PreProcess/B04_PreProcess_Api_Fetch.ts @@ -325,8 +325,16 @@ export interface VWorldMeta { lon_max: number; lat_min: number; lat_max: number; - /** 그림 상자 경위도 n×n 고른 마디의 TM 값(`[j * n + i]` · j 남 → 북 · 589-1) — 서버가 응답 때 붙임. */ - tm_grid?: { n: number; x: number[]; y: number[] }; + /** 그림 상자 둘레 경위도 n×n 고른 마디의 TM 값(`[j * n + i]` · j 남 → 북 · 589-1) — 서버가 응답 때 붙임. */ + tm_grid?: { + n: number; + lon_min: number; + lon_max: number; + lat_min: number; + lat_max: number; + x: number[]; + y: number[]; + }; } export function getVWorldMapUrl(projectId: string, layerName: string): string { diff --git a/B04_PreProcess/B04_PreProcess_Engine_VWorld.py b/B04_PreProcess/B04_PreProcess_Engine_VWorld.py index 08f8031e5..81d8758a3 100644 --- a/B04_PreProcess/B04_PreProcess_Engine_VWorld.py +++ b/B04_PreProcess/B04_PreProcess_Engine_VWorld.py @@ -197,40 +197,54 @@ def download_vworld_satellite_map( return map_png_path -# 그림 상자 경위도 격자 한 변 마디 수 — 쌍선형 나머지 용화 0.001 · D 0.006 · 큰 상자 0.014 m(589-1). -TM_GRID_NODES = 9 +# 경위도 격자 — 그림 상자와 둘레 한 폭씩(도엽이 상자 밖 0.8 폭까지 · 3 × 3 폭) · 한 변 25 마디 +# = 상자 안 8 등분 · 쌍선형 나머지 용화 0.001 · D 0.006 · 큰 상자 0.014 m(589-1). +TM_GRID_NODES = 25 +TM_GRID_MARGIN = 1.0 def tm_grid_for_meta(meta: dict, crs_input: str, nodes: int = TM_GRID_NODES) -> dict | None: - """그림 상자 경위도 고른 마디의 TM 값(589-1 · 화면이 경위도 겹을 TM 자리로 놓음). + """그림 상자 둘레 경위도 고른 마디의 TM 값(589-1 · 화면이 경위도 겹을 TM 자리로 놓음). - 마디 `[j * n + i]` = 경도 `lon_min + i/(n-1)·폭` · 위도 `lat_min + j/(n-1)·높이`(j 는 남 → 북). - 왼위 · 오른아래 마디가 메타 `x_min · y_max` · `x_max · y_min` 과 1 cm 넘게 다르면(메타를 다른 + 격자 범위 = 상자 경위도 범위를 둘레로 `TM_GRID_MARGIN` 폭씩 넓힌 것 · 마디 `[j * n + i]` = + 경도 `lon_min + i/(n-1)·폭` · 위도 `lat_min + j/(n-1)·높이`(격자 범위 · j 는 남 → 북). + 상자 왼위 · 오른아래가 메타 `x_min · y_max` · `x_max · y_min` 과 1 cm 넘게 다르면(메타를 다른 좌표계로 만든 것) 격자를 주지 않음 — 화면은 옛 같은 네모 식으로 남음. """ keys = ("lon_min", "lon_max", "lat_min", "lat_max", "x_min", "x_max", "y_min", "y_max") if any(not isinstance(meta.get(key), (int, float)) for key in keys): return None transformer = Transformer.from_crs("EPSG:4326", crs_input, always_xy=True) - lon_span = meta["lon_max"] - meta["lon_min"] - lat_span = meta["lat_max"] - meta["lat_min"] - lons = [meta["lon_min"] + i / (nodes - 1) * lon_span for i in range(nodes)] - lats = [meta["lat_min"] + j / (nodes - 1) * lat_span for j in range(nodes)] - xs: list[float] = [] - ys: list[float] = [] - for lat in lats: - x_row, y_row = transformer.transform(lons, [lat] * nodes) - xs.extend(float(v) for v in x_row) - ys.extend(float(v) for v in y_row) - top_left = nodes * (nodes - 1) - bottom_right = nodes - 1 + (x0, x1), (y1, y0) = transformer.transform( + [meta["lon_min"], meta["lon_max"]], [meta["lat_max"], meta["lat_min"]] + ) off = max( - abs(xs[top_left] - meta["x_min"]), - abs(ys[top_left] - meta["y_max"]), - abs(xs[bottom_right] - meta["x_max"]), - abs(ys[bottom_right] - meta["y_min"]), + abs(x0 - meta["x_min"]), + abs(y1 - meta["y_max"]), + abs(x1 - meta["x_max"]), + abs(y0 - meta["y_min"]), ) if not math.isfinite(off) or off > 0.01: logger.warning("위성 메타 귀퉁이와 TM 격자가 %.3f m 어긋남 — 격자 안 줌", off) return None - return {"n": nodes, "x": [round(v, 4) for v in xs], "y": [round(v, 4) for v in ys]} + lon_pad = (meta["lon_max"] - meta["lon_min"]) * TM_GRID_MARGIN + lat_pad = (meta["lat_max"] - meta["lat_min"]) * TM_GRID_MARGIN + lon_min, lon_max = meta["lon_min"] - lon_pad, meta["lon_max"] + lon_pad + lat_min, lat_max = meta["lat_min"] - lat_pad, meta["lat_max"] + lat_pad + lons = [lon_min + i / (nodes - 1) * (lon_max - lon_min) for i in range(nodes)] + xs: list[float] = [] + ys: list[float] = [] + for j in range(nodes): + lat = lat_min + j / (nodes - 1) * (lat_max - lat_min) + x_row, y_row = transformer.transform(lons, [lat] * nodes) + xs.extend(float(v) for v in x_row) + ys.extend(float(v) for v in y_row) + return { + "n": nodes, + "lon_min": lon_min, + "lon_max": lon_max, + "lat_min": lat_min, + "lat_max": lat_max, + "x": [round(v, 4) for v in xs], + "y": [round(v, 4) for v in ys], + } diff --git a/B04_PreProcess/logic/B04_PreProcess_UI_MapRender_Prepare.ts b/B04_PreProcess/logic/B04_PreProcess_UI_MapRender_Prepare.ts index 4af2541e6..ea4251e72 100644 --- a/B04_PreProcess/logic/B04_PreProcess_UI_MapRender_Prepare.ts +++ b/B04_PreProcess/logic/B04_PreProcess_UI_MapRender_Prepare.ts @@ -61,8 +61,8 @@ export function normOfLonLat(normalizer: Normalizer, lon: number, lat: number): } /** - * 경위도 → TM 정규화(589-1) — 서버 `tm_grid`(그림 상자 경위도 고른 마디의 TM 값) 쌍선형 → - * `((x − x_min)/W, 1 − (y − y_min)/H)` = `metricToScreen` 과 같은 자. 상자 밖은 가장자리 칸을 늘려 씀. + * 경위도 → TM 정규화(589-1) — 서버 `tm_grid`(그림 상자 둘레 경위도 고른 마디의 TM 값) 쌍선형 → + * `((x − x_min)/W, 1 − (y − y_min)/H)` = `metricToScreen` 과 같은 자. 격자 밖은 가장자리 칸을 늘려 씀. * 격자가 없거나 모양이 틀리면 null. */ export function tmGridNormOf( @@ -73,13 +73,13 @@ export function tmGridNormOf( if (!grid || n < 2 || grid.x.length !== n * n || grid.y.length !== n * n) return null; const { x: gx, y: gy } = grid; const cells = n - 1; - const lonRange = meta.lon_max - meta.lon_min || 1; - const latRange = meta.lat_max - meta.lat_min || 1; + const lonRange = grid.lon_max - grid.lon_min || 1; + const latRange = grid.lat_max - grid.lat_min || 1; const spanX = meta.width_meters || 1; const spanY = meta.height_meters || 1; return (lon, lat) => { - const u = ((lon - meta.lon_min) / lonRange) * cells; - const v = ((lat - meta.lat_min) / latRange) * cells; + const u = ((lon - grid.lon_min) / lonRange) * cells; + const v = ((lat - grid.lat_min) / latRange) * cells; const i = Math.min(Math.max(Math.floor(u), 0), cells - 1); const j = Math.min(Math.max(Math.floor(v), 0), cells - 1); const fu = u - i; diff --git a/resources/tester/test_589_tm_grid.py b/resources/tester/test_589_tm_grid.py index b03f0e321..6f7b319f9 100644 --- a/resources/tester/test_589_tm_grid.py +++ b/resources/tester/test_589_tm_grid.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- """589-1 B04 · B05 지도 — 경위도 겹(도엽 · 지적 · 유역 · 위성 그림)을 TM 겹(노선 · 관) 자리에. -- 서버 `tm_grid_for_meta` 마디 = pyproj · 왼위 · 오른아래 마디 = 메타 귀퉁이 1 mm 안(Bessel · GRS80) +- 서버 `tm_grid_for_meta` 마디(상자 둘레 한 폭씩 · 25×25) = pyproj · 상자 귀퉁이 마디 = 메타 + 귀퉁이 1 mm 안(Bessel · GRS80) - 메타 귀퉁이와 다른 좌표계면 격자 안 줌 -- TS `normOfLonLat`(격자 쌍선형) = pyproj TM 자리 0.02 m 안 · 격자 없으면 옛 같은 네모 식 그대로 +- TS `normOfLonLat`(격자 쌍선형) = pyproj TM 자리 0.02 m 안(상자 밖 0.9 폭까지) · 격자 없으면 옛 + 같은 네모 식 그대로 - 위성 그림 행렬(화면 가운데 1차) = 그림 px → 경위도 → pyproj TM 자리와 화면 안 0.5 px 안 - 옛 같은 네모 꼴은 귀퉁이에서 용화 ≥ 3 m · D ≥ 60 m 어긋남(고친 까닭) @@ -86,10 +88,11 @@ def _run(site, with_grid=True): if with_grid: meta["tm_grid"] = tm_grid_for_meta(meta, crs) lon_min, lon_max, lat_min, lat_max = box + # 상자 안 · 둘레(도엽은 상자 밖 0.8 폭까지 나감) — -0.9 ~ 1.9 폭 lonlat = [ (lon_min + a / 10 * (lon_max - lon_min), lat_min + b / 10 * (lat_max - lat_min)) - for a in range(11) - for b in range(11) + for a in range(-9, 20) + for b in range(-9, 20) ] payload = {"meta": meta, "lonlat": lonlat, "image": size, "views": VIEWS} proc = subprocess.run( @@ -113,17 +116,18 @@ def test_서버_격자는_pyproj_마디(site): name, meta, _, _ = site grid = meta["tm_grid"] n = grid["n"] - assert n == 9 and len(grid["x"]) == n * n - assert abs(grid["x"][n * (n - 1)] - meta["x_min"]) < 1e-3 - assert abs(grid["y"][n * (n - 1)] - meta["y_max"]) < 1e-3 - assert abs(grid["x"][n - 1] - meta["x_max"]) < 1e-3 - assert abs(grid["y"][n - 1] - meta["y_min"]) < 1e-3 + assert n == 25 and len(grid["x"]) == n * n + # 격자 = 상자 둘레 한 폭씩 → 상자 왼위 = (8, 16) 마디 · 오른아래 = (16, 8) 마디 + assert abs(grid["x"][16 * n + 8] - meta["x_min"]) < 1e-3 + assert abs(grid["y"][16 * n + 8] - meta["y_max"]) < 1e-3 + assert abs(grid["x"][8 * n + 16] - meta["x_max"]) < 1e-3 + assert abs(grid["y"][8 * n + 16] - meta["y_min"]) < 1e-3 fwd = Transformer.from_crs("EPSG:4326", SITES[name][0], always_xy=True) - i, j = 3, 5 - lon = meta["lon_min"] + i / (n - 1) * (meta["lon_max"] - meta["lon_min"]) - lat = meta["lat_min"] + j / (n - 1) * (meta["lat_max"] - meta["lat_min"]) - x, y = fwd.transform(lon, lat) - assert math.hypot(grid["x"][j * n + i] - x, grid["y"][j * n + i] - y) < 1e-3 + for i, j in ((3, 5), (0, 24), (24, 0)): + lon = grid["lon_min"] + i / (n - 1) * (grid["lon_max"] - grid["lon_min"]) + lat = grid["lat_min"] + j / (n - 1) * (grid["lat_max"] - grid["lat_min"]) + x, y = fwd.transform(lon, lat) + assert math.hypot(grid["x"][j * n + i] - x, grid["y"][j * n + i] - y) < 1e-3 def test_다른_좌표계_메타면_격자_안_줌():