158 lines
5.8 KiB
Python
158 lines
5.8 KiB
Python
"""Provision governed DHMV II terrain/surface rasters for the persisted Mol Area.
|
|
|
|
The operator calls the canonical GeoIntel DHMV acquisition API. The backend
|
|
performs the bounded official WCS request, exact Area clipping, validation,
|
|
checksum storage and Dataset/Job persistence. No raster rows are written
|
|
directly by this script.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
from typing import Any, Iterable
|
|
|
|
import requests
|
|
|
|
|
|
DEFAULT_API_URL = "http://127.0.0.1:8000"
|
|
DEFAULT_PROJECT_NAME = "Kempen Regional Workbench"
|
|
DEFAULT_AREA_FRAGMENT = "Gemeente Mol"
|
|
PRODUCTS = ("dtm_1m", "dsm_1m")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Provision official DHMV II DTM/DSM rasters for Mol.")
|
|
parser.add_argument("--base-url", default=os.environ.get("GEOINTEL_INTERNAL_API_URL", DEFAULT_API_URL))
|
|
parser.add_argument("--project-name", default=DEFAULT_PROJECT_NAME)
|
|
parser.add_argument("--area-name", default=DEFAULT_AREA_FRAGMENT)
|
|
parser.add_argument("--products", default=",".join(PRODUCTS), help="Comma-separated governed product keys.")
|
|
parser.add_argument("--resolution-m", type=float, default=5.0)
|
|
parser.add_argument("--timeout", type=int, default=1800)
|
|
parser.add_argument("--force", action="store_true")
|
|
return parser.parse_args()
|
|
|
|
|
|
def unwrap(response: requests.Response) -> Any:
|
|
response.raise_for_status()
|
|
payload = response.json()
|
|
if not isinstance(payload, dict) or "data" not in payload:
|
|
raise RuntimeError(f"Non-canonical API response from {response.url}")
|
|
return payload["data"]
|
|
|
|
|
|
def coordinates(geometry: dict[str, Any]) -> Iterable[tuple[float, float]]:
|
|
def walk(value: Any):
|
|
if isinstance(value, list) and len(value) >= 2 and all(isinstance(item, (int, float)) for item in value[:2]):
|
|
yield float(value[0]), float(value[1])
|
|
return
|
|
if isinstance(value, list):
|
|
for child in value:
|
|
yield from walk(child)
|
|
|
|
yield from walk(geometry.get("coordinates", []))
|
|
|
|
|
|
def geometry_bbox(geometry: dict[str, Any]) -> dict[str, float | str]:
|
|
points = list(coordinates(geometry))
|
|
if not points:
|
|
raise RuntimeError("Persisted Area geometry contains no coordinates")
|
|
xs = [point[0] for point in points]
|
|
ys = [point[1] for point in points]
|
|
return {
|
|
"min_x": min(xs),
|
|
"min_y": min(ys),
|
|
"max_x": max(xs),
|
|
"max_y": max(ys),
|
|
"crs": "EPSG:4326",
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
base_url = args.base_url.rstrip("/")
|
|
session = requests.Session()
|
|
session.headers.update({"User-Agent": "GeoIntel-DHMV-Operator/1.0"})
|
|
|
|
projects = unwrap(session.get(f"{base_url}/api/v1/projects", params={"limit": 200, "offset": 0}, timeout=60))["items"]
|
|
project = next((item for item in projects if item["name"] == args.project_name), None)
|
|
if project is None:
|
|
raise RuntimeError(f"Project {args.project_name!r} was not found")
|
|
|
|
areas = unwrap(
|
|
session.get(
|
|
f"{base_url}/api/v1/projects/{project['id']}/areas",
|
|
params={"limit": 200, "offset": 0},
|
|
timeout=60,
|
|
)
|
|
)["items"]
|
|
fragment = args.area_name.casefold()
|
|
area = next((item for item in areas if fragment in item["name"].casefold()), None)
|
|
if area is None:
|
|
raise RuntimeError(f"Area containing {args.area_name!r} was not found")
|
|
bbox = geometry_bbox(area["geometry"])
|
|
|
|
requested_products = [item.strip() for item in args.products.split(",") if item.strip()]
|
|
invalid = sorted(set(requested_products) - set(PRODUCTS))
|
|
if invalid:
|
|
raise RuntimeError(f"Unsupported DHMV product keys: {', '.join(invalid)}")
|
|
|
|
results = []
|
|
for product_key in requested_products:
|
|
job = unwrap(
|
|
session.post(
|
|
f"{base_url}/api/v1/projects/{project['id']}/datasets/dhmv/acquire",
|
|
json={
|
|
"bbox": bbox,
|
|
"area_id": area["id"],
|
|
"product_key": product_key,
|
|
"resolution_m": args.resolution_m,
|
|
"force_refresh": args.force,
|
|
},
|
|
timeout=args.timeout,
|
|
)
|
|
)
|
|
if job.get("status") != "success" or not job.get("output_dataset_id"):
|
|
raise RuntimeError(f"DHMV acquisition failed for {product_key}: {job.get('error_message') or job}")
|
|
analysis = unwrap(
|
|
session.post(
|
|
f"{base_url}/api/v1/projects/{project['id']}/datasets/{job['output_dataset_id']}/raster/terrain/select",
|
|
json={"bbox": bbox, "area_id": area["id"]},
|
|
timeout=args.timeout,
|
|
)
|
|
)
|
|
if sorted(analysis.get("unsupported_metrics", [])) != ["water_depth_m", "water_volume_m3"]:
|
|
raise RuntimeError("DHMV terrain contract must explicitly keep water depth and volume unavailable")
|
|
results.append(
|
|
{
|
|
"product_key": product_key,
|
|
"dataset_id": job["output_dataset_id"],
|
|
"reused": bool((job.get("result_json") or {}).get("reused")),
|
|
"resolution_m": analysis["resolution_m"],
|
|
"sample_count": analysis["sample_count"],
|
|
"coverage_ratio": analysis["coverage_ratio"],
|
|
"metrics": analysis["summary"]["metrics"],
|
|
}
|
|
)
|
|
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"status": "ok",
|
|
"project_id": project["id"],
|
|
"area_id": area["id"],
|
|
"area_name": area["name"],
|
|
"bbox": bbox,
|
|
"products": results,
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|