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.
164 lines
5.2 KiB
Python
164 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the production Earth boundary PMTiles artifact from collected sources."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime as dt
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
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", type=Path, required=True)
|
|
parser.add_argument("--manifest", type=Path, required=True)
|
|
parser.add_argument("--build-input-hash", required=True)
|
|
parser.add_argument("--pov-policy", type=Path, default=None)
|
|
parser.add_argument("--min-zoom", type=int, default=0)
|
|
parser.add_argument("--max-zoom", type=int, default=10)
|
|
return parser.parse_args()
|
|
|
|
|
|
def require_tool(name: str) -> str:
|
|
path = shutil.which(name)
|
|
if not path:
|
|
raise RuntimeError(f"required external tool not found in PATH: {name}")
|
|
return path
|
|
|
|
|
|
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") != "FeatureCollection":
|
|
raise ValueError(f"{path} must be a GeoJSON FeatureCollection")
|
|
return payload
|
|
|
|
|
|
def iter_layer_features(payload: dict[str, Any], layer: str) -> list[dict[str, Any]]:
|
|
out = []
|
|
for feature in payload.get("features") or []:
|
|
if not feature.get("geometry"):
|
|
continue
|
|
props = dict(feature.get("properties") or {})
|
|
props["tippecanoe"] = {"layer": layer}
|
|
out.append(
|
|
{
|
|
"type": "Feature",
|
|
"geometry": feature["geometry"],
|
|
"properties": props,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def write_ndjson(path: Path, features: list[dict[str, Any]]) -> None:
|
|
with path.open("w", encoding="utf-8") as f:
|
|
for feature in features:
|
|
f.write(json.dumps(feature, ensure_ascii=False, separators=(",", ":")))
|
|
f.write("\n")
|
|
|
|
|
|
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 main() -> None:
|
|
args = parse_args()
|
|
tippecanoe = require_tool("tippecanoe")
|
|
pmtiles = require_tool("pmtiles")
|
|
|
|
admin0 = load_geojson(args.admin0_source)
|
|
coastline = load_geojson(args.coastline_source)
|
|
claims = load_geojson(args.claims_source)
|
|
features = [
|
|
*iter_layer_features(admin0, "boundary_admin0"),
|
|
*iter_layer_features(coastline, "coastline"),
|
|
*iter_layer_features(claims, "claim_line"),
|
|
]
|
|
if not features:
|
|
raise RuntimeError("no features available for PMTiles build")
|
|
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.manifest.parent.mkdir(parents=True, exist_ok=True)
|
|
with tempfile.TemporaryDirectory(prefix="planet-earth-boundaries-") as tmp:
|
|
tmp_dir = Path(tmp)
|
|
ndjson_path = tmp_dir / "earth-boundaries.ndjson"
|
|
mbtiles_path = tmp_dir / "earth-boundaries.mbtiles"
|
|
write_ndjson(ndjson_path, features)
|
|
|
|
subprocess.run(
|
|
[
|
|
tippecanoe,
|
|
"--force",
|
|
"--read-parallel",
|
|
"--minimum-zoom",
|
|
str(args.min_zoom),
|
|
"--maximum-zoom",
|
|
str(args.max_zoom),
|
|
"--no-tile-compression",
|
|
"--output",
|
|
str(mbtiles_path),
|
|
str(ndjson_path),
|
|
],
|
|
check=True,
|
|
)
|
|
subprocess.run([pmtiles, "convert", str(mbtiles_path), str(args.output)], check=True)
|
|
|
|
manifest = {
|
|
"version": "china-pov-v1",
|
|
"builtAt": dt.datetime.now(dt.UTC).replace(microsecond=0).isoformat(),
|
|
"tileProvider": "pmtiles-mvt",
|
|
"format": "pmtiles+mvt",
|
|
"buildInputHash": args.build_input_hash,
|
|
"pmtiles": {
|
|
"path": os.path.relpath(args.output, args.manifest.parent),
|
|
"sha256": sha256_file(args.output),
|
|
},
|
|
"tiles": {
|
|
"minZoom": args.min_zoom,
|
|
"maxZoom": args.max_zoom,
|
|
},
|
|
"layers": ["boundary_admin0", "boundary_disputed_internal", "coastline", "claim_line"],
|
|
"povPolicy": {
|
|
"path": str(args.pov_policy) if args.pov_policy else None,
|
|
"applied": False,
|
|
"note": "This builder packages collected sources. Full China POV geometry overlay is still a source-prep responsibility.",
|
|
},
|
|
"base": None,
|
|
"hoverIndex": None,
|
|
}
|
|
with args.manifest.open("w", encoding="utf-8") as f:
|
|
json.dump(manifest, f, ensure_ascii=False, indent=2)
|
|
f.write("\n")
|
|
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"status": "success",
|
|
"output": str(args.output),
|
|
"features": len(features),
|
|
"format": "pmtiles+mvt",
|
|
},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|