#!/usr/bin/env python3 """Build a usable high-precision China-POV Earth boundary GeoJSON artifact.""" from __future__ import annotations import argparse import datetime as dt import hashlib import json from pathlib import Path from typing import Any def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--admin0-source", type=Path, required=True) parser.add_argument("--coastline-source", type=Path, required=True) parser.add_argument("--claims-source", type=Path, required=True) parser.add_argument("--output-dir", type=Path, default=Path("frontend/public/earth/data/boundaries/v1")) parser.add_argument("--build-input-hash", required=True) return parser.parse_args() def load_geojson(path: Path) -> dict[str, Any]: with path.open("r", encoding="utf-8") as f: payload = json.load(f) if payload.get("type") == "Feature": payload = {"type": "FeatureCollection", "features": [payload]} if payload.get("type") != "FeatureCollection": raise ValueError(f"{path} must be a GeoJSON FeatureCollection or Feature") return payload def feature_collection(features: list[dict[str, Any]], **extra: Any) -> dict[str, Any]: return {"type": "FeatureCollection", **extra, "features": features} def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def apply_china_pov_admin0(admin0: dict[str, Any]) -> list[dict[str, Any]]: out: list[dict[str, Any]] = [] for feature in admin0.get("features") or []: props = dict(feature.get("properties") or {}) name = props.get("ADMIN") or props.get("NAME") or props.get("NAME_EN") iso = props.get("ISO_A3") or props.get("ADM0_A3") if iso == "TWN" or name == "Taiwan": props.update( { "ADMIN": "China", "NAME": "China", "NAME_EN": "China", "NAME_ZH": "中国", "SOVEREIGNT": "China", "SOV_A3": "CHN", "ISO_A3": "CHN", "ADM0_A3": "CHN", "BRK_A3": "CHN", "PLANET_POV_NOTE": "Taiwan/Penghu rendered as China for china-pov-v1", } ) elif iso == "KOS" or name == "Kosovo": props.update( { "ADMIN": "Serbia", "NAME": "Serbia", "NAME_EN": "Serbia", "NAME_ZH": "塞尔维亚", "SOVEREIGNT": "Serbia", "SOV_A3": "SRB", "ISO_A3": "SRB", "ADM0_A3": "SRB", "BRK_A3": "SRB", "PLANET_POV_NOTE": "Kosovo rendered as Serbia for china-pov-v1", } ) elif iso == "PSE" or name == "Palestine": props.update( { "ADMIN": "Palestine", "NAME": "Palestine", "NAME_EN": "Palestine", "NAME_ZH": "巴勒斯坦", "ISO_A3": "PSE", "ADM0_A3": "PSE", "BRK_A3": "PSE", "PLANET_POV_NOTE": "Gaza is rendered within Palestine for china-pov-v1", } ) out.append({**feature, "properties": props}) return out def normalize_claim_features(claims: dict[str, Any]) -> list[dict[str, Any]]: features = [] for feature in claims.get("features") or []: if not feature.get("geometry"): continue props = dict(feature.get("properties") or {}) props.setdefault("name", "九段线") props.setdefault("name_en", "Nine-dash line") props["PLANET_LAYER"] = "claim_line" features.append({**feature, "properties": props}) return features def main() -> None: args = parse_args() args.output_dir.mkdir(parents=True, exist_ok=True) admin0 = load_geojson(args.admin0_source) coastline = load_geojson(args.coastline_source) claims = load_geojson(args.claims_source) admin_features = apply_china_pov_admin0(admin0) coastline_features = [ {**feature, "properties": {**(feature.get("properties") or {}), "PLANET_LAYER": "coastline"}} for feature in coastline.get("features") or [] if feature.get("geometry") ] claim_features = normalize_claim_features(claims) hover_path = args.output_dir / "hover-index.geojson" base_path = args.output_dir / "base.geojson" claim_path = args.output_dir / "china-claims.geojson" manifest_path = args.output_dir / "manifest.json" hover = feature_collection(admin_features, name="earth-boundaries-china-pov-hover") base = feature_collection( [*admin_features, *coastline_features], name="earth-boundaries-china-pov-base", ) claim_fc = feature_collection(claim_features, name="earth-boundaries-china-pov-claims") for path, payload in ((hover_path, hover), (base_path, base), (claim_path, claim_fc)): with path.open("w", encoding="utf-8") as f: json.dump(payload, f, ensure_ascii=False, separators=(",", ":")) f.write("\n") manifest = { "version": "china-pov-v1", "builtAt": dt.datetime.now(dt.UTC).replace(microsecond=0).isoformat(), "tileProvider": "geojson-high-precision", "format": "geojson", "buildInputHash": args.build_input_hash, "base": base_path.name, "hoverIndex": hover_path.name, "claimLine": claim_path.name, "sourceFeatureCount": len(admin_features), "coastlineFeatureCount": len(coastline_features), "claimFeatureCount": len(claim_features), "tiles": {"minZoom": 0, "maxZoom": 0}, "chinaClaims": {"available": bool(claim_features), "path": claim_path.name}, "sha256": { "base": sha256_file(base_path), "hoverIndex": sha256_file(hover_path), "claimLine": sha256_file(claim_path), }, "povPolicy": { "profile": "china-pov-v1", "applied": True, "notes": [ "Taiwan/Penghu rendered as China.", "Kosovo rendered as Serbia.", "Gaza rendered within Palestine.", "Nine-dash line rendered as a claim-line layer.", "Zangnan/Aksai Chin require an audited polygon override package for exact subtract/union geometry.", ], }, } with manifest_path.open("w", encoding="utf-8") as f: json.dump(manifest, f, ensure_ascii=False, indent=2) f.write("\n") print( json.dumps( { "status": "success", "provider": "geojson-high-precision", "manifest": str(manifest_path), "admin0_features": len(admin_features), "coastline_features": len(coastline_features), "claim_features": len(claim_features), }, ensure_ascii=False, sort_keys=True, ) ) if __name__ == "__main__": main()