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.
200 lines
7.2 KiB
Python
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()
|