docs(pbf): archive Full production provenance and engineering history

This commit is contained in:
OpenAI Codex
2026-10-13 14:02:56 +08:00
parent 1fef4c6dd1
commit 07d16c38a8
381 changed files with 107826 additions and 527 deletions

View File

@@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""生成当前 NavSea PBF 的可取舍数据字典和分类统计。"""
from __future__ import annotations
import argparse
import csv
import json
from collections import Counter, defaultdict
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path
from typing import Any
import mapbox_vector_tile
TRACKED_FIELDS = (
"class_code",
"display_code",
"shape_class_code",
"canonical_object_type",
"object_family",
"detection_class",
"chart_symbol_code",
"chart_label_position_code",
"chart_label_subtext",
"chart_icon_image",
"chart_fill_pattern",
"chart_fill_style",
"chart_text_color",
"chart_text_style",
"chart_line_color",
"chart_line_width",
"chart_line_pattern",
"arc_id",
"arc_source_code",
"light_color_code",
"light_sector_mode",
"icon_id",
"source_layer",
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--pbf-root",
type=Path,
default=Path("/home/wwwroot/pbf-delivery-full-semantic-unified-20260808"),
)
parser.add_argument(
"--style",
type=Path,
default=Path("src/pbf/style.navsea-delivery-full-semantic-unified-20260808.json"),
)
parser.add_argument(
"--report-root",
type=Path,
default=Path("report/current_pbf_inventory_full_semantic_20260809"),
)
parser.add_argument("--workers", type=int, default=4)
return parser.parse_args()
def empty_result() -> dict[str, Any]:
return {
"tiles": Counter(),
"layers": Counter(),
"geometries": Counter(),
"property_keys": Counter(),
"property_key_layers": Counter(),
"values": Counter(),
"value_layers": Counter(),
}
def scan_chunk(paths: list[str]) -> dict[str, Any]:
result = empty_result()
for raw_path in paths:
path = Path(raw_path)
rel = path.relative_to(path.parents[2])
zoom = rel.parts[0]
result["tiles"][f"z{zoom}"] += 1
decoded = mapbox_vector_tile.decode(path.read_bytes())
for layer_name, layer in decoded.items():
features = layer.get("features", [])
result["layers"][layer_name] += len(features)
for feature in features:
geometry_type = (feature.get("geometry") or {}).get("type", "<none>")
result["geometries"][(layer_name, geometry_type)] += 1
properties = feature.get("properties") or {}
for key, value in properties.items():
result["property_keys"][key] += 1
result["property_key_layers"][(key, layer_name)] += 1
if key not in TRACKED_FIELDS or value is None:
continue
value_text = str(value)
result["values"][(key, value_text)] += 1
result["value_layers"][(key, value_text, layer_name)] += 1
return result
def merge(results: list[dict[str, Any]]) -> dict[str, Counter]:
merged = empty_result()
for result in results:
for key in merged:
merged[key].update(result[key])
return merged
def style_get_fields(style: dict[str, Any]) -> set[str]:
fields: set[str] = set()
def walk(value: Any) -> None:
if isinstance(value, dict):
for child in value.values():
walk(child)
elif isinstance(value, list):
if (
len(value) >= 2
and isinstance(value[0], str)
and value[0] in {"get", "has"}
and isinstance(value[1], str)
):
fields.add(value[1])
for child in value:
walk(child)
walk(style)
return fields
def write_csv(path: Path, fieldnames: list[str], rows: list[dict[str, Any]]) -> None:
with path.open("w", encoding="utf-8-sig", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
def main() -> None:
args = parse_args()
paths = sorted(args.pbf_root.glob("*/*/*.pbf"))
if not paths:
raise SystemExit(f"no PBF tiles under {args.pbf_root}")
chunk_size = 128
chunks = [
[str(path) for path in paths[start : start + chunk_size]]
for start in range(0, len(paths), chunk_size)
]
with ProcessPoolExecutor(max_workers=args.workers) as executor:
results = list(executor.map(scan_chunk, chunks))
stats = merge(results)
args.report_root.mkdir(parents=True, exist_ok=True)
style = json.loads(args.style.read_text(encoding="utf-8"))
style_fields = style_get_fields(style)
layer_rows = []
for layer, feature_count in stats["layers"].most_common():
geometries = Counter(
{
geometry: count
for (layer_name, geometry), count in stats["geometries"].items()
if layer_name == layer
}
)
fields = sorted(
key
for (key, layer_name), count in stats["property_key_layers"].items()
if layer_name == layer and count
)
layer_rows.append(
{
"layer": layer,
"feature_count": feature_count,
"geometry_counts": json.dumps(geometries, ensure_ascii=False),
"property_count": len(fields),
"properties": ",".join(fields),
}
)
write_csv(
args.report_root / "图层分类.csv",
["layer", "feature_count", "geometry_counts", "property_count", "properties"],
layer_rows,
)
field_rows = []
for key, count in stats["property_keys"].most_common():
layer_counts = Counter(
{
layer: layer_count
for (field, layer), layer_count in stats["property_key_layers"].items()
if field == key
}
)
field_rows.append(
{
"field": key,
"feature_value_count": count,
"layer_count": len(layer_counts),
"layers": json.dumps(layer_counts, ensure_ascii=False),
"used_by_current_style": "yes" if key in style_fields else "no",
}
)
write_csv(
args.report_root / "字段分类.csv",
["field", "feature_value_count", "layer_count", "layers", "used_by_current_style"],
field_rows,
)
value_rows = []
for (field, value), count in stats["values"].most_common():
layer_counts = Counter(
{
layer: layer_count
for (tracked_field, tracked_value, layer), layer_count in stats["value_layers"].items()
if tracked_field == field and tracked_value == value
}
)
value_rows.append(
{
"field": field,
"value": value,
"feature_count": count,
"layers": json.dumps(layer_counts, ensure_ascii=False),
}
)
write_csv(
args.report_root / "字段值分类.csv",
["field", "value", "feature_count", "layers"],
value_rows,
)
total_features = sum(stats["layers"].values())
summary = {
"pbf_root": str(args.pbf_root),
"tile_count": len(paths),
"tiles_by_zoom": dict(sorted(stats["tiles"].items())),
"feature_count": total_features,
"layer_count": len(stats["layers"]),
"property_count": len(stats["property_keys"]),
"style_get_fields": sorted(style_fields),
}
(args.report_root / "summary.json").write_text(
json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
md: list[str] = [
"# 当前统一 Full PBF 数据分类报告",
"",
f"- PBF 根目录:`{args.pbf_root}`",
f"- 瓦片数:`{len(paths)}`",
f"- feature 总数:`{total_features}`",
f"- 图层数:`{len(stats['layers'])}`",
f"- 属性字段数:`{len(stats['property_keys'])}`",
"",
"## 按 zoom 的瓦片数量",
"",
"| zoom | 瓦片数 |",
"|---|---:|",
]
md.extend(f"| {zoom} | {count} |" for zoom, count in sorted(stats["tiles"].items()))
md.extend(["", "## 图层分类", "", "| 图层 | feature 数 | 几何类型 | 字段数 |", "|---|---:|---|---:|"])
for row in layer_rows:
md.append(
f"| `{row['layer']}` | {row['feature_count']} | `{row['geometry_counts']}` | {row['property_count']} |"
)
md.extend(
[
"",
"## 取舍说明",
"",
"- `geometry`、图层名、`canonical_object_type`、分类/显示码、`chart_*`、`arc_id`/`arc_source_code` 是渲染、对象审计或灯弧追溯的核心数据,不能在未做专项审计前删除。",
"- 只被点击详情或外部业务使用、但不被当前 style 读取的字段,属于可选保留项;应先确认 pickup、审计和后续重建是否依赖。",
"- 仅存在于 PBF、当前 style 不读取且不参与对象身份/分类/追溯的字段,才是候选删除项;具体字段和数量见 `字段分类.csv`。",
"- 所有细分值见 `字段值分类.csv`,不要仅依据当前出现次数为 0 的 style 分支删除上游分类能力。",
"",
"## 明细文件",
"",
"- `图层分类.csv`:每个 source-layer 的 feature、几何和字段分布。",
"- `字段分类.csv`:每个属性字段的出现量、所在图层和当前 style 是否读取。",
"- `字段值分类.csv`:分类码、显示码、对象类型、图片、纹理和灯弧等字段的逐值统计。",
]
)
(args.report_root / "当前PBF数据分类报告.md").write_text("\n".join(md) + "\n", encoding="utf-8")
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()