- 새 common_util_vector_tiles: SHP(cp949 · .prj) → 3857 색인 GeoPackage(data_forest/_index) → z8~15 MVT · 원자료마다 규칙 한 줄(줌 · 단순화 · 작은 면 거름 · 남길 칸) · 창고 storage/_shared/vector_tiles/<name>/v1_<지문> · 615 꼴 잠금 · .part
- 새 common_util_wms_tiles: WMS → 256 px PNG(3857 bbox) · 처음 받고 storage/_shared/wms_tiles 기억 · 빈 · 오류 = 투명 PNG 300 s 기억 · 키 없음 = 404 · 키 안 샘
- 새 D03_PlanAnalysis_Router_Tiles: /plan/vt/{name}/{z}/{x}/{y}.pbf · /plan/wmt/{name}/{z}/{x}/{y}.png · main.py 등록
- 새 tools/precompute_vector_tiles.py: 원자료 이름 · 줌 · 경위도 상자 · 일꾼
- 시험 test_622_vector_tiles.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.9 KiB
Python
109 lines
4.9 KiB
Python
# -*- 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())
|