# -*- coding: utf-8 -*- """받은 산림 자료 벡터 조각 미리 만들기(PLAN 622-1) — 줌아웃 조각(z8~12)을 공용 창고에 미리. 실행: ./venv/Scripts/python.exe tools/precompute_vector_tiles.py forest_road --workers 2 --zooms 8-12 줌 범위 · --bbox 129.0,36.7,129.3,36.9 경위도 상자만(시험 지역) --status 남은 수 · 크기만 - 원자료 이름 = `common_util_vector_tiles.RULES`(forest_road · forest_soil · eco_forest). - 색인(GeoPackage) 먼저 한 번 → 조각 목록 = 도형 상자가 닿는 z/x/y(가장 큰 줌에서 세고 위 줌은 부모) · 낮은 줌부터. - 셈 = `ensure_tile`(창고에 있으면 건너뜀 = 이어 하기 · 잠금 · `.part` → 바꿈) · 낮은 우선순위 · 일꾼 여럿. """ from __future__ import annotations import argparse import sys import time from concurrent.futures import ProcessPoolExecutor, as_completed from pathlib import Path import numpy as np ROOT = Path(__file__).resolve().parent.parent if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from common_util import common_util_vector_tiles as vt # noqa: E402 from tools.precompute_terrain import below_normal # noqa: E402 def tiles_for(bounds: np.ndarray, zooms: range) -> list[tuple[int, int, int]]: """도형 상자(4 × N · 3857)가 닿는 조각(낮은 줌부터).""" top = zooms.stop - 1 size = vt.WORLD_M / 2**top half = vt.WORLD_M / 2 x0 = np.floor((bounds[0] + half) / size).astype(np.int64) x1 = np.floor((bounds[2] + half) / size).astype(np.int64) y0 = np.floor((half - bounds[3]) / size).astype(np.int64) y1 = np.floor((half - bounds[1]) / size).astype(np.int64) found: set[tuple[int, int]] = set() for a, b, c, d in zip(x0, x1, y0, y1, strict=True): found.update((x, y) for x in range(a, b + 1) for y in range(c, d + 1)) tiles = {(z, x >> (top - z), y >> (top - z)) for z in zooms for x, y in found} return sorted(t for t in tiles if vt.in_bounds(*t)) def build(job: tuple[str, int, int, int]) -> tuple[tuple[str, int, int, int], int, float]: started = time.monotonic() size = len(vt.ensure_tile(*job)) return job, size, round(time.monotonic() - started, 2) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="받은 산림 자료 벡터 조각 미리 만들기(PLAN 622-1)") parser.add_argument("name", choices=sorted(vt.RULES)) parser.add_argument("--zooms", default="8-12") parser.add_argument("--workers", type=int, default=2) parser.add_argument("--bbox", default="", help="경위도 서,남,동,북 — 이 상자에 닿는 도형만") parser.add_argument("--status", action="store_true") args = parser.parse_args(argv) lo, _, hi = args.zooms.partition("-") zooms = range(max(int(lo), vt.RULES[args.name].min_zoom), int(hi or lo) + 1) if int(lo) < vt.MIN_ZOOM or zooms.stop - 1 > vt.MAX_ZOOM: parser.error(f"줌 = {vt.MIN_ZOOM}~{vt.MAX_ZOOM}") import pyogrio from pyproj import Transformer below_normal() started = time.monotonic() index = vt.ensure_index(args.name) folder = vt._folder(args.name) if index is None or folder is None: raise SystemExit(f"원자료 없음: {vt.DATA_ROOT} {vt.RULES[args.name].globs}") print(f"색인 {index.name} · {time.monotonic() - started:.0f} s", flush=True) _, bounds = pyogrio.read_bounds(index, layer=args.name) if args.bbox: w, s, e, n = (float(v) for v in args.bbox.split(",")) to_merc = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True) (mw, me), (ms, mn) = to_merc.transform([w, e], [s, n]) hit = (bounds[2] >= mw) & (bounds[0] <= me) & (bounds[3] >= ms) & (bounds[1] <= mn) bounds = bounds[:, hit] tiles = tiles_for(bounds, zooms) todo = [t for t in tiles if not vt.tile_path(folder, *t).is_file()] print(f"조각 {len(tiles)} · 남음 {len(todo)} · 창고 {folder} · 일꾼 {args.workers}", flush=True) if not args.status and todo: started = time.monotonic() with ProcessPoolExecutor(args.workers, initializer=below_normal) as pool: futures = [pool.submit(build, (args.name, *t)) for t in todo] for n, future in enumerate(as_completed(futures), 1): (_, z, x, y), size, seconds = future.result() if n % 50 == 0 or n == len(todo): elapsed = time.monotonic() - started print( f"{n}/{len(todo)} z{z}/{x}/{y} {size} B {seconds} s · {elapsed:.0f} s", flush=True, ) for z in zooms: paths = [vt.tile_path(folder, *t) for t in tiles if t[0] == z] have = [p.stat().st_size for p in paths if p.is_file()] print(f"z{z} 조각 {len(paths)} · 있음 {len(have)} · {sum(have) / 1e6:.1f} MB", flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())