Files
planet/scripts/build_earth_boundary_china_pov_geojson.py
linkong 93eb41a9f7
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.58.0
Release 0.58.0 includes the Earth high-precision boundary PMTiles/MVT pipeline, standardized Earth boundary source collectors, China POV boundary configuration templates, and removal of the legacy low-precision GeoJSON fallback. It also adds Earth news target-location queueing/archive support, fixes datasource task status visibility, documents the Earth surface depth-spacing rules that prevent far-zoom z-fighting snow/black blocks, and updates bilingual operations/developer docs.
2026-05-15 17:40:07 +08:00

200 lines
7.2 KiB
Python

#!/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()