158 lines
5.5 KiB
Python
Executable File
158 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from collections import Counter
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import mapbox_vector_tile
|
|
|
|
|
|
DYNAMIC_FINAL_FIELDS = {
|
|
"s52_is_safety_contour",
|
|
"s52_sounding_prefix",
|
|
"s52_sounding_is_shallow",
|
|
"s52_isolated_danger",
|
|
"s52_display_base_promoted",
|
|
}
|
|
|
|
|
|
def decode_features(path: Path) -> list[tuple[str, dict[str, Any]]]:
|
|
decoded = mapbox_vector_tile.decode(path.read_bytes())
|
|
features: list[tuple[str, dict[str, Any]]] = []
|
|
for layer_name, layer in decoded.items():
|
|
for feature in layer.get("features", []):
|
|
features.append((layer_name, dict(feature.get("properties") or {})))
|
|
return features
|
|
|
|
|
|
def audit_runtime(features: list[tuple[str, dict[str, Any]]]) -> dict[str, Any]:
|
|
runtime = [
|
|
(layer, props)
|
|
for layer, props in features
|
|
if props.get("s52_mariner_dependent") == 1
|
|
]
|
|
failures: list[str] = []
|
|
if not runtime:
|
|
failures.append("没有发现 s52_mariner_dependent=1 Feature")
|
|
|
|
csp_counts = Counter(str(props.get("s52_runtime_csp") or "") for _, props in runtime)
|
|
leaked = Counter()
|
|
for _, props in runtime:
|
|
csp = str(props.get("s52_runtime_csp") or "")
|
|
if not csp:
|
|
failures.append("runtime Feature 缺少 s52_runtime_csp")
|
|
if csp and csp not in str(props.get("s52_csp") or "").split(","):
|
|
failures.append(f"{csp} 未保留在 s52_csp")
|
|
for key in DYNAMIC_FINAL_FIELDS:
|
|
if key in props:
|
|
leaked[key] += 1
|
|
if csp in {"DEPARE01", "DEPARE02"}:
|
|
if "depth_min_m" not in props:
|
|
failures.append(f"{csp} 缺少 depth_min_m")
|
|
if "s52_area_color" in props:
|
|
leaked["DEPARE:s52_area_color"] += 1
|
|
elif csp == "DEPCNT02" and "contour_depth_m" not in props:
|
|
failures.append("DEPCNT02 缺少 contour_depth_m")
|
|
elif csp == "SOUNDG02" and "s52_sounding_depth_m" not in props:
|
|
failures.append("SOUNDG02 缺少 s52_sounding_depth_m")
|
|
|
|
if leaked:
|
|
failures.append(f"发现 runtime 最终状态泄漏: {dict(leaked)}")
|
|
|
|
return {
|
|
"feature_count": len(features),
|
|
"mariner_dependent_count": len(runtime),
|
|
"runtime_csp_counts": dict(csp_counts),
|
|
"layer_counts": dict(Counter(layer for layer, _ in runtime)),
|
|
"fact_counts": {
|
|
key: sum(1 for _, props in features if key in props)
|
|
for key in (
|
|
"depth_min_m",
|
|
"depth_max_m",
|
|
"contour_depth_m",
|
|
"s52_sounding_depth_m",
|
|
"s52_hazard_depth_m",
|
|
"s52_surrounding_max_drval1_m",
|
|
"s52_surrounding_min_drval2_m",
|
|
)
|
|
},
|
|
"dynamic_final_leaks": dict(leaked),
|
|
"failures": failures,
|
|
"status": "PASS" if not failures else "FAIL",
|
|
}
|
|
|
|
|
|
def audit_reference(features: list[tuple[str, dict[str, Any]]]) -> dict[str, Any]:
|
|
failures: list[str] = []
|
|
runtime_markers = sum(
|
|
1 for _, props in features if props.get("s52_mariner_dependent") == 1
|
|
)
|
|
if runtime_markers:
|
|
failures.append(f"reference PBF 出现 {runtime_markers} 个 runtime marker")
|
|
return {
|
|
"feature_count": len(features),
|
|
"runtime_marker_count": runtime_markers,
|
|
"fixed_result_counts": {
|
|
"safety_contour": sum(
|
|
1 for _, props in features if props.get("s52_is_safety_contour") == 1
|
|
),
|
|
"isolated_danger": sum(
|
|
1 for _, props in features if props.get("s52_isolated_danger") == 1
|
|
),
|
|
"display_base_promoted": sum(
|
|
1 for _, props in features if props.get("s52_display_base_promoted") == 1
|
|
),
|
|
"sounding_prefix": sum(
|
|
1 for _, props in features if "s52_sounding_prefix" in props
|
|
),
|
|
},
|
|
"presentation_sequence_counts": {
|
|
key: sum(1 for _, props in features if key in props)
|
|
for key in (
|
|
"s52_symbol_count",
|
|
"s52_line_count",
|
|
"s52_symbol_rotation_deg",
|
|
"s52_symbol_rule_sequence",
|
|
"s52_priority_override",
|
|
)
|
|
},
|
|
"failures": failures,
|
|
"status": "PASS" if not failures else "FAIL",
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="检查 CM95 S-52 runtime/reference Delivery PBF 边界"
|
|
)
|
|
parser.add_argument("--runtime-pbf", type=Path, required=True)
|
|
parser.add_argument("--reference-pbf", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
runtime = audit_runtime(decode_features(args.runtime_pbf))
|
|
reference = audit_reference(decode_features(args.reference_pbf))
|
|
result = {
|
|
"schema": "cm95-s52-runtime-contract-check/v1",
|
|
"runtime_pbf": str(args.runtime_pbf),
|
|
"reference_pbf": str(args.reference_pbf),
|
|
"runtime": runtime,
|
|
"reference": reference,
|
|
"status": "PASS"
|
|
if runtime["status"] == reference["status"] == "PASS"
|
|
else "FAIL",
|
|
}
|
|
text = json.dumps(result, ensure_ascii=False, indent=2)
|
|
print(text)
|
|
if args.output:
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(text + "\n", encoding="utf-8")
|
|
return 0 if result["status"] == "PASS" else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|