186 lines
6.0 KiB
Python
186 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from collections import Counter, defaultdict
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import mapbox_vector_tile
|
|
import yaml
|
|
|
|
|
|
DELIVERY_ALLOWLIST = {
|
|
"canonical_object_type",
|
|
"class_code",
|
|
"chart_fill_pattern",
|
|
"chart_fill_style",
|
|
"chart_icon_image",
|
|
"chart_label_position_code",
|
|
"chart_label_subtext",
|
|
"chart_label_text",
|
|
"chart_line_color",
|
|
"chart_line_width",
|
|
"chart_symbol_code",
|
|
"chart_text_color",
|
|
"chart_text_style",
|
|
"clearance_height_m",
|
|
"depth_value_m",
|
|
"display_code",
|
|
"least_depth_m",
|
|
"light_color_code",
|
|
"light_sector_mode",
|
|
"arc_id",
|
|
"arc_source_code",
|
|
"name_ja",
|
|
"place_name_en",
|
|
"shape_class_code",
|
|
}
|
|
|
|
SPRITE_FIELDS = {"chart_icon_image", "chart_fill_pattern", "chart_line_pattern", "arc_id"}
|
|
|
|
|
|
def parse_args():
|
|
p = argparse.ArgumentParser()
|
|
p.add_argument("--pbf-root", required=True)
|
|
p.add_argument("--style", required=True)
|
|
p.add_argument("--sprite-json", required=True)
|
|
p.add_argument("--policy", default="config/pbf/cm95_tile_policy_v1.yaml")
|
|
p.add_argument("--profile", choices=["delivery", "engineering"], default="delivery")
|
|
p.add_argument("--output", default="cm95_pbf_output_audit_v1.json")
|
|
return p.parse_args()
|
|
|
|
|
|
def main():
|
|
a = parse_args()
|
|
root = Path(a.pbf_root)
|
|
style = json.loads(Path(a.style).read_text(encoding="utf-8"))
|
|
sprite = json.loads(Path(a.sprite_json).read_text(encoding="utf-8"))
|
|
policy = yaml.safe_load(Path(a.policy).read_text(encoding="utf-8"))
|
|
|
|
style_layers = {
|
|
layer["source-layer"]
|
|
for layer in style.get("layers", [])
|
|
if layer.get("source") == "navsea_delivery" and layer.get("source-layer")
|
|
}
|
|
sprite_keys = set(sprite)
|
|
|
|
layer_counts = Counter()
|
|
extent_counts = Counter()
|
|
unknown_layers = Counter()
|
|
property_failures = []
|
|
sprite_failures = []
|
|
decode_failures = []
|
|
feature_ids = defaultdict(set)
|
|
duplicate_ids = []
|
|
|
|
files = sorted(root.rglob("*.pbf"))
|
|
for path in files:
|
|
try:
|
|
z = int(path.relative_to(root).parts[0])
|
|
decoded = mapbox_vector_tile.decode(path.read_bytes())
|
|
except Exception as exc:
|
|
decode_failures.append({"file": str(path), "error": str(exc)})
|
|
continue
|
|
|
|
expected_extent = int(
|
|
policy.get("extent_by_zoom", {}).get(str(z), policy.get("default_extent", 4096))
|
|
)
|
|
|
|
for layer_name, payload in decoded.items():
|
|
features = payload.get("features", [])
|
|
layer_counts[layer_name] += len(features)
|
|
extent = int(payload.get("extent") or 4096)
|
|
extent_counts[f"z{z}:{extent}"] += 1
|
|
|
|
if extent != expected_extent:
|
|
decode_failures.append(
|
|
{
|
|
"file": str(path),
|
|
"layer": layer_name,
|
|
"error": f"extent {extent} != expected {expected_extent}",
|
|
}
|
|
)
|
|
|
|
if layer_name not in style_layers and not (
|
|
a.profile == "engineering" and layer_name == "cm95_unmapped"
|
|
):
|
|
unknown_layers[layer_name] += len(features)
|
|
|
|
for f in features:
|
|
fid = f.get("id")
|
|
if fid is not None:
|
|
key = (str(path), layer_name)
|
|
if fid in feature_ids[key]:
|
|
duplicate_ids.append(
|
|
{"file": str(path), "layer": layer_name, "id": fid}
|
|
)
|
|
feature_ids[key].add(fid)
|
|
|
|
props = f.get("properties") or {}
|
|
if a.profile == "delivery":
|
|
extras = sorted(set(props) - DELIVERY_ALLOWLIST)
|
|
if extras:
|
|
property_failures.append(
|
|
{
|
|
"file": str(path),
|
|
"layer": layer_name,
|
|
"id": fid,
|
|
"extra_properties": extras,
|
|
}
|
|
)
|
|
|
|
for field in SPRITE_FIELDS:
|
|
value = props.get(field)
|
|
if value and value not in sprite_keys:
|
|
# arc_id may intentionally reference arc sprite; same JSON key contract applies.
|
|
sprite_failures.append(
|
|
{
|
|
"file": str(path),
|
|
"layer": layer_name,
|
|
"id": fid,
|
|
"field": field,
|
|
"value": value,
|
|
}
|
|
)
|
|
|
|
failure_count = (
|
|
len(decode_failures)
|
|
+ sum(unknown_layers.values())
|
|
+ len(property_failures)
|
|
+ len(sprite_failures)
|
|
+ len(duplicate_ids)
|
|
)
|
|
|
|
result = {
|
|
"schema": "navsea.cm95.pbf-output-audit/v1",
|
|
"profile": a.profile,
|
|
"pbf_file_count": len(files),
|
|
"feature_count": sum(layer_counts.values()),
|
|
"layer_counts": dict(layer_counts),
|
|
"extent_counts": dict(extent_counts),
|
|
"unknown_layers": dict(unknown_layers),
|
|
"decode_or_extent_failures": decode_failures,
|
|
"delivery_property_failures": property_failures,
|
|
"sprite_failures": sprite_failures,
|
|
"duplicate_feature_ids_within_layer_tile": duplicate_ids,
|
|
"failure_count": failure_count,
|
|
"overall": "PASS" if failure_count == 0 else "FAIL",
|
|
}
|
|
|
|
Path(a.output).write_text(
|
|
json.dumps(result, ensure_ascii=False, indent=2),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
print(f"PBF files : {result['pbf_file_count']}")
|
|
print(f"Features : {result['feature_count']}")
|
|
print(f"Layers : {len(layer_counts)}")
|
|
print(f"Failures : {failure_count}")
|
|
print(f"OVERALL : {result['overall']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|