feat(공용 지형): 띠 밖 좌표계 = 띠 지형 쌍선형 옮겨 담기 (PLAN 614-1 · 요구 494)
- ensure_sheet_terrain: 도엽 띠(5185~5187) 아닌 좌표계면 띠 저장본(없으면 띠로 먼저 셈)을 그 좌표계 2 m 격자로 쌍선형 옮겨 담아 저장 · 열쇠 via = 띠 - 이미 직접 센 저장본 그대로 · REPROJECT_FROM_BAND 한 줄로 끔 · 가장자리 = 창고에 있는 이웃 띠만 · 없으면 직선 연장 - 문턱(브레인) = 참값(등고선 위 점) 대비 오차 · 옮김이 더하는 95 % ≤ 0.10 m · 최대 ≤ 직접 최대 - 시험 test_614 새로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
도엽 + 여유 200 m 등고선으로 셈 → 도엽 안쪽 칸(칸 중심 위경도가 그 도엽)만 저장 → 이어 붙이면
|
||||
통째 셈과 같음. 격자 원점 = 좌표 격자 배수(칸 모서리 = k·cell_m).
|
||||
열쇠 = 계산 방식 판 + 여유 + 전국 파일 지문(크기 · 수정 시각) + 쓴 도엽 zip 해시.
|
||||
띠 밖 좌표계(5176 · 띠 아닌 5186 등)는 그 도엽 띠(5185~5187) 지형을 쌍선형으로 옮겨 담음(614) —
|
||||
열쇠에 `via` = 띠 · 이미 직접 센 저장본은 그대로 씀.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -32,6 +34,7 @@ from B04_PreProcess.B04_PreProcess_Engine_MapSheet import (
|
||||
)
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Grid import ELEVATION_KEYS, GridSpec
|
||||
from common_util.common_util_atomic import atomic_write_npz
|
||||
from common_util.common_util_crs import tm_epsg_for_lon
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_terrain_surface import (
|
||||
TERRAIN_ALGO,
|
||||
@@ -47,6 +50,9 @@ NATIONAL_GPKG = Path(PROJECT_ROOT) / "resources" / "data_global_contours" / "nat
|
||||
TERRAIN_MARGIN_M = 200.0
|
||||
# ponytail: 잠금 파일이 이보다 오래되면 죽은 셈으로 보고 깸 · 도엽 하나 셈 1~5 분
|
||||
LOCK_STALE_S = 15 * 60
|
||||
# 띠 밖 좌표계 = 띠 지형 옮겨 담기(614) · 직접 셈 대비 문턱 못 넘으면 False(직접 셈)
|
||||
# 문턱 = 참값(등고선 위 점) 대비 오차 — 옮김이 더하는 95 % ≤ 0.10 m · 최대 ≤ 직접 셈 최대
|
||||
REPROJECT_FROM_BAND = True
|
||||
|
||||
Progress = Callable[[int, str], None] | None
|
||||
Box = tuple[float, float, float, float]
|
||||
@@ -65,6 +71,15 @@ def _names(sheet_no: str, epsg: int, cell_m: float) -> tuple[Path, Path, Path]:
|
||||
return folder / f"{stem}.npz", folder / f"{stem}.key.json", folder / f".{stem}.lock"
|
||||
|
||||
|
||||
def _via(sheet_no: str, epsg: int | str) -> int | None:
|
||||
"""옮겨 담을 띠 좌표계 — 이미 띠 · 옮기기 꺼짐 · 번호 없는 좌표계면 None(직접 셈)."""
|
||||
if not REPROJECT_FROM_BAND or not str(epsg).isdigit():
|
||||
return None
|
||||
lon0, _, lon1, _ = sheet5k_to_bounds(sheet_no)
|
||||
band = tm_epsg_for_lon((lon0 + lon1) / 2)
|
||||
return None if band == int(epsg) else band
|
||||
|
||||
|
||||
def _gpkg_stamp() -> str | None:
|
||||
try:
|
||||
stat = NATIONAL_GPKG.stat()
|
||||
@@ -100,6 +115,8 @@ def _stored_ok(sheet_no: str, epsg: int, cell_m: float) -> dict[str, Any] | None
|
||||
return None
|
||||
if not stored.get("empty") and not npz.is_file():
|
||||
return None
|
||||
if stored.get("via") is not None and stored["via"] != _via(sheet_no, epsg):
|
||||
return None
|
||||
zips = {s: _sha256(TERRAIN_ROOT / s / f"{s}.zip") for s in stored.get("zips") or {}}
|
||||
now = _key(epsg, cell_m, zips)
|
||||
if now["gpkg"] is None: # 전국 파일 없는 PC — 있는 저장본을 믿음(시놀로지 덮어쓰기 핑퐁 막음)
|
||||
@@ -282,6 +299,8 @@ def ensure_sheet_terrain(
|
||||
stored = _stored_ok(sheet_no, epsg, cell_m) # 기다리는 동안 남이 셌을 수 있음
|
||||
if stored is not None:
|
||||
return None if stored.get("empty") else npz
|
||||
if (band := _via(sheet_no, epsg)) is not None:
|
||||
return _reproject(sheet_no, epsg, band, cell_m, report)
|
||||
started = time.monotonic()
|
||||
report(2, f"전국 등고선 읽기 · {sheet_no}")
|
||||
ix0, iy0, own = _owned(sheet_no, epsg, cell_m)
|
||||
@@ -348,6 +367,75 @@ def ensure_sheet_terrain(
|
||||
return npz
|
||||
|
||||
|
||||
def _extend_edges(z: np.ndarray) -> np.ndarray:
|
||||
"""빈 칸 중 값 칸 바로 옆 한 칸 = 안쪽 두 칸으로 직선 연장(가로 뒤 세로 · 모서리 포함) —
|
||||
이웃 띠 지형이 없는 도엽 가장자리에서도 쌍선형 네 칸을 채움."""
|
||||
for axis in (1, 0):
|
||||
src = np.moveaxis(z, axis, -1)
|
||||
out = src.copy()
|
||||
inner, outer = src[..., 1:-1], (src[..., 2:], src[..., :-2])
|
||||
for side, far in ((slice(None, -2), outer[0]), (slice(2, None), outer[1])):
|
||||
gap = np.isnan(out[..., side]) & np.isfinite(inner) & np.isfinite(far)
|
||||
out[..., side][gap] = (2 * inner - far)[gap]
|
||||
z = np.moveaxis(out, -1, axis)
|
||||
return z
|
||||
|
||||
|
||||
def _bilinear(grid: dict[str, Any], px: np.ndarray, py: np.ndarray) -> np.ndarray:
|
||||
"""격자(셀 중심 · y 오름차순) 위 점들의 쌍선형 높이 — 네 칸 중 빈 칸이 있으면 NaN."""
|
||||
z, cell = _extend_edges(grid["z"]), grid["cell_m"]
|
||||
fx, fy = (px - grid["x"][0]) / cell, (py - grid["y"][0]) / cell
|
||||
i = np.clip(np.floor(fx).astype(np.int64), 0, z.shape[1] - 2)
|
||||
j = np.clip(np.floor(fy).astype(np.int64), 0, z.shape[0] - 2)
|
||||
tx, ty = fx - i, fy - j
|
||||
return (z[j, i] * (1 - tx) + z[j, i + 1] * tx) * (1 - ty) + (
|
||||
z[j + 1, i] * (1 - tx) + z[j + 1, i + 1] * tx
|
||||
) * ty
|
||||
|
||||
|
||||
def _reproject(
|
||||
sheet_no: str, epsg: int, band: int, cell_m: float, report: Callable[[int, str], None]
|
||||
) -> Path | None:
|
||||
"""띠 지형(없으면 띠로 먼저 셈) → 이 좌표계 격자로 쌍선형 옮겨 담아 저장 — 잠금 안에서 부름.
|
||||
|
||||
가장자리 칸은 창고에 있는 이웃 띠 지형만 씀(없으면 직선 연장 · 새로 안 셈).
|
||||
"""
|
||||
started = time.monotonic()
|
||||
npz, key_path, _ = _names(sheet_no, epsg, cell_m)
|
||||
src = ensure_sheet_terrain(sheet_no, band, cell_m, lambda p, s: report(p * 80 // 100, s))
|
||||
band_key = json.loads(_names(sheet_no, band, cell_m)[1].read_text(encoding="utf-8"))
|
||||
key = {**_key(epsg, cell_m, band_key.get("zips") or {}), "via": band}
|
||||
key_path.unlink(missing_ok=True) # 쓰는 도중 끊기면 옛 열쇠가 새 파일을 덮지 않게
|
||||
if src is None:
|
||||
npz.unlink(missing_ok=True)
|
||||
atomic_write_json(key_path, {**key, "empty": True})
|
||||
return None
|
||||
report(85, f"좌표계 옮겨 담기 · {sheet_no}")
|
||||
ix0, iy0, own = _owned(sheet_no, epsg, cell_m)
|
||||
rows, cols = np.nonzero(own)
|
||||
to_band = Transformer.from_crs(_crs(epsg), _crs(band), always_xy=True)
|
||||
px, py = to_band.transform((ix0 + cols + 0.5) * cell_m, (iy0 + rows + 0.5) * cell_m)
|
||||
pad = 2 * cell_m
|
||||
box = (px.min() - pad, py.min() - pad, px.max() + pad, py.max() + pad)
|
||||
near = [n for n in sheets_for_box(box, band) if n == sheet_no or _stored_ok(n, band, cell_m)]
|
||||
core = np.full(own.shape, np.nan, dtype=np.float32)
|
||||
core[rows, cols] = _bilinear(load_terrain(band, sheets=near, box=box, cell_m=cell_m), px, py)
|
||||
atomic_write_npz(npz, z=core, ix0=np.int64(ix0), iy0=np.int64(iy0), cell_m=np.float64(cell_m))
|
||||
atomic_write_json(
|
||||
key_path,
|
||||
{**key, "source": "reproject", "seconds": round(time.monotonic() - started, 1)},
|
||||
)
|
||||
report(100, f"완료 · {sheet_no}")
|
||||
logger.info(
|
||||
"공용 지형 %s 옮겨 담음 EPSG:%s → %s %.1fs",
|
||||
sheet_no,
|
||||
band,
|
||||
epsg,
|
||||
time.monotonic() - started,
|
||||
)
|
||||
return npz
|
||||
|
||||
|
||||
def load_terrain(
|
||||
epsg: int,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user