from __future__ import annotations import json import os import shutil import subprocess from concurrent.futures import ProcessPoolExecutor from pathlib import Path import mapbox_vector_tile from navsea_tile_reencode_guard import decode_tile_with_extents, encode_layers_preserving_extents BASE_ROOT = Path("/home/wwwroot/pbf-delivery-full-20260418-rebuild") OUTPUT_ROOT = Path("/home/wwwroot/pbf-delivery-full-semantic-unified-20260808") MAP_JSON = Path(__file__).resolve().parent / "src/pbf/semantic_sprite_key_map_2026-08-08.json" FIELDS = ("chart_icon_image", "chart_fill_pattern", "chart_line_pattern") def hardlink_or_copy(src: Path, dst: Path) -> None: dst.parent.mkdir(parents=True, exist_ok=True) if dst.exists(): dst.unlink() try: os.link(src, dst) except OSError: shutil.copy2(src, dst) def rewrite_one(args): src_str, dst_str, value_map = args src = Path(src_str) dst = Path(dst_str) raw = src.read_bytes() decoded, extents = decode_tile_with_extents(raw) changed = 0 layers = [] for layer_name, layer in decoded.items(): features = [] for feature in layer.get("features", []): props = dict(feature.get("properties") or {}) feature_changed = False for field in FIELDS: value = props.get(field) if isinstance(value, str) and value in value_map: props[field] = value_map[value] feature_changed = True if feature_changed: changed += 1 out = {"geometry": feature["geometry"], "properties": props} if feature.get("id") is not None: out["id"] = feature["id"] features.append(out) layers.append({"name": layer_name, "features": features}) if not changed: hardlink_or_copy(src, dst) return 0, 0 dst.parent.mkdir(parents=True, exist_ok=True) # The output tree is initially made with hard links to BASE_ROOT. Break # that link before writing so the legacy baseline can never be modified. if dst.exists(): dst.unlink() dst.write_bytes(encode_layers_preserving_extents(layers, extents)) return 1, changed def main() -> None: mapping = json.loads(MAP_JSON.read_text(encoding="utf-8")) mapping["fill-daytime-405"] = mapping["fill-daytime-401"] if OUTPUT_ROOT.exists(): shutil.rmtree(OUTPUT_ROOT) OUTPUT_ROOT.mkdir(parents=True, exist_ok=True) subprocess.run(["cp", "-al", f"{BASE_ROOT}/.", str(OUTPUT_ROOT)], check=True) command = ["rg", "-a", "-l", "--no-messages", "-F"] for key in mapping: command.extend(["-e", key]) command.append(str(BASE_ROOT)) result = subprocess.run(command, check=True, capture_output=True, text=True) paths = [Path(line) for line in result.stdout.splitlines() if line.strip()] jobs = [(str(src), str(OUTPUT_ROOT / src.relative_to(BASE_ROOT)), mapping) for src in paths] changed_tiles = changed_features = 0 with ProcessPoolExecutor(max_workers=12) as executor: for tile_changed, feature_count in executor.map(rewrite_one, jobs, chunksize=64): changed_tiles += tile_changed changed_features += feature_count print(json.dumps({"base_tiles": len(list(BASE_ROOT.rglob("*.pbf"))), "scanned_tiles": len(paths), "changed_tiles": changed_tiles, "changed_features": changed_features, "output": str(OUTPUT_ROOT)}, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()