#!/usr/bin/env python3 """只读按原始 PBF Feature 重放 Composite Visual Object;不推断 PRIMARY/语义。""" from __future__ import annotations import argparse from collections import Counter, defaultdict import hashlib import importlib.util import json from pathlib import Path from typing import Any from PIL import Image from mapbox_vector_tile.Mapbox import vector_tile_pb2 def load_instantiator() -> Any: path = Path(__file__).resolve().parents[1] / "visual-object-instantiation" / "instantiate_pbf_visual_objects.py" spec = importlib.util.spec_from_file_location("provider_actual_visual_instantiation", path) if spec is None or spec.loader is None: raise RuntimeError("无法加载实际 PBF Visual Object 实例化器") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def replay_catalog( catalog: dict[str, Any], style: dict[str, Any], sprite_meta: dict[str, Any], sprite_hashes: dict[str, str], pbf_root: Path, *, instantiator: Any = None, max_objects: int | None = None, ) -> dict[str, Any]: """Replay the recorded example raw Feature; retain explicit limits of one sample per VO.""" engine = instantiator or load_instantiator() objects = catalog.get("visual_objects") style_layers = style.get("layers") if not isinstance(objects, list) or not isinstance(style_layers, list): raise ValueError("Catalog visual_objects 或 Style layers 无效") indexed: dict[str, list[tuple[int, dict[str, Any]]]] = defaultdict(list) for index, layer in enumerate(style_layers): if isinstance(layer, dict) and isinstance(layer.get("source-layer"), str): indexed[layer["source-layer"]].append((index, layer)) source_owners = {source_layer: {layer.get('source') for _, layer in members} for source_layer, members in indexed.items()} cache: dict[str, dict[str, Any]] = {} results: list[dict[str, Any]] = [] seen_ids: set[str] = set() for obj in sorted(objects, key=lambda row: row.get("visual_object_id", ""))[:max_objects]: object_id = obj.get("visual_object_id") if not isinstance(object_id, str) or not object_id or object_id in seen_ids: raise ValueError("Catalog Visual Object ID 无效或重复") seen_ids.add(object_id) provenance = obj.get("provenance") or {} source_layer = provenance.get("source_layer") example = obj.get("example") or {} tile = example.get("tile") fid = example.get("feature_id") status = None diagnostics: list[str] = [] replayed: list[dict[str, Any]] = [] if not isinstance(source_layer, str) or not source_layer or not isinstance(tile, str) or len(tile.split("/")) != 3: status = "INVALID_CATALOG_EXAMPLE" elif type(fid) is not int or fid < 0: status = "UNVERIFIABLE_FEATURE_ID" elif len(source_owners.get(source_layer, set())) != 1 or None in source_owners.get(source_layer, set()): # The PBF source is not encoded in the catalog's example identity. status = "AMBIGUOUS_STYLE_SOURCE" else: z, x, y = tile.split("/") if not all(part.isdigit() for part in (z, x, y)): status = "INVALID_CATALOG_EXAMPLE" else: if tile not in cache: path = pbf_root / z / x / (y + ".pbf") if not path.is_file(): cache[tile] = {} else: raw = vector_tile_pb2.tile() raw.ParseFromString(path.read_bytes()) name_counts = Counter(layer.name for layer in raw.layers) cache[tile] = {layer.name: list(layer.features) for layer in raw.layers} cache[tile + ":duplicates"] = {name for name, count in name_counts.items() if count != 1} # Retain original tile layer metadata for key-value decoding. cache[tile + ":rawLayers"] = {layer.name: layer for layer in raw.layers} feature_list = cache[tile].get(source_layer) if source_layer in cache.get(tile + ":duplicates", set()): status = "DUPLICATE_PBF_SOURCE_LAYER" elif feature_list is None: status = "PBF_TILE_OR_SOURCE_LAYER_UNAVAILABLE" else: candidates = [feature for feature in feature_list if feature.HasField("id") and int(feature.id) == fid] if len(candidates) != 1: status = "FEATURE_ID_NOT_UNIQUE" if len(candidates) > 1 else "FEATURE_ID_NOT_FOUND" else: raw_feature = candidates[0] tile_layer = cache[tile + ":rawLayers"][source_layer] props = engine.decode_feature_properties(tile_layer, raw_feature) for index, layer in indexed.get(source_layer, []): if not engine.matches(layer, props, int(z)): continue replayed.extend(engine.component_from_layer( layer, index, props, int(z), sprite_meta, sprite_hashes )) raw_signature = { "geometry_role": engine.geometry_role(raw_feature.type), "components": engine.signature_components(replayed), } replayed_id = engine.stable_id("VO", raw_signature) if not replayed: status = "NO_RENDER_COMPONENT_REPLAYED" elif replayed_id != object_id: status = "COMPOSITE_SIGNATURE_MISMATCH" diagnostics.append("原始 PBF 重放得到的视觉签名与 Catalog ID 不一致") elif engine.signature_components(replayed) != engine.signature_components(obj.get("components", [])): status = "CATALOG_COMPONENT_MISMATCH" else: status = "SAME_RAW_FEATURE_REPLAY_CONFIRMED" results.append({ "visualObjectId": object_id, "status": status, "example": {"tile": tile, "sourceLayer": source_layer, "featureId": fid}, "componentCount": len(obj.get("components", [])), "replayedComponentCount": len(replayed), "sameRawFeature": status == "SAME_RAW_FEATURE_REPLAY_CONFIRMED", "primaryComponentAuthorized": False, "displaySelectorAuthorized": False, "diagnostics": diagnostics, }) counts = Counter(record["status"] for record in results) return { "schemaVersion": "navsea-feature-lineage-replay-audit/v1", "scope": "ONE_RECORDED_EXAMPLE_PER_VISUAL_OBJECT_NOT_FULL_COVERAGE", "visualObjectCount": len(results), "sampleTileCount": len([k for k in cache if ":" not in k]), "statusCounts": dict(sorted(counts.items())), "sameRawFeatureConfirmedCount": counts["SAME_RAW_FEATURE_REPLAY_CONFIRMED"], "primaryComponentAuthorizedCount": 0, "displaySelectorAuthorizedCount": 0, "results": results, } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--catalog", type=Path, required=True) parser.add_argument("--style", type=Path, required=True) parser.add_argument("--sprite-json", type=Path, required=True) parser.add_argument("--sprite-png", type=Path, required=True) parser.add_argument("--pbf-root", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--max-objects", type=int) args = parser.parse_args() catalog = json.loads(args.catalog.read_text(encoding="utf-8")) style_bytes = args.style.read_bytes() style = json.loads(style_bytes.decode("utf-8")) if catalog.get("schema") != "actual-visual-object-catalog/v1": raise ValueError("Catalog schema 不受支持") catalog_style = (catalog.get("inputs") or {}).get("style") if catalog_style: source_style_path = Path(catalog_style) if not source_style_path.is_file(): raise ValueError("Catalog 原始 Style 不存在,不可交叉校验") if hashlib.sha256(source_style_path.read_bytes()).digest() != hashlib.sha256(style_bytes).digest(): raise ValueError("Catalog Style 与待验证 Style SHA-256 不一致") if args.max_objects is not None and args.max_objects <= 0: raise ValueError("max-objects 必须大于零") sprites = json.loads(args.sprite_json.read_text(encoding="utf-8")) with Image.open(args.sprite_png) as atlas: sprite_hashes = load_instantiator().sprite_hashes(sprites, atlas.convert("RGBA")) result = replay_catalog( catalog, style, sprites, sprite_hashes, args.pbf_root, max_objects=args.max_objects, ) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({ "scope": result["scope"], "visualObjectCount": result["visualObjectCount"], "statusCounts": result["statusCounts"], "primaryAuthorized": 0, "displayAuthorized": 0, "output": str(args.output), }, ensure_ascii=False)) return 0 if result["sameRawFeatureConfirmedCount"] == result["visualObjectCount"] else 2 if __name__ == "__main__": raise SystemExit(main())