fix(B04·B05): 위성 메타 TM 격자를 상자 둘레 한 폭씩 넓힘 — 25×25 (PLAN 589-1)

- 도엽이 위성 상자 밖 0.8 폭까지 나가 9×9 격자 늘려 씀 자리에서 0.33 m 어긋남(화면 실측)
- 격자 범위 = 상자 ± 한 폭(3 × 3 폭) · 상자 안 칸 크기 그대로(8 등분) · 격자 자기 경위도 범위 칸 더함
- 지킴 시험 = 상자 밖 0.9 폭까지 pyproj 0.02 m 안

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 20:54:08 +09:00
co-authored by Claude Opus 5.5
parent ba18b0f96d
commit 9c2b5e151b
4 changed files with 70 additions and 44 deletions
+10 -2
View File
@@ -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 {
+36 -22
View File
@@ -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],
}
@@ -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;