Files
Aislo/tools/precompute_vector_tiles.py
T
eomsangdonandClaude Opus 5.5 251dfd9a5b feat(D03): 받은 산림 · 환경 자료 줌별 조각 — 벡터(임도 · 산림토양도 · 경제림) · 그림 대리(생태자연도 · 산사태) (PLAN 622-1 · 요구 529)
- 새 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>
2026-10-31 19:16:05 +09:00

109 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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())