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,219 @@
#!/usr/bin/env python3
"""
NavSea port overlay export v1.0
从九州工程库快照导出港湾/渔港叠加预览资产:
- 港湾 AOI 面
- 港湾 AOI 中心点
- 元数据清单
用途:
- 在 MapLibre 中快速检查港湾、渔港、小港湾的覆盖范围
- 作为后续港湾语义栅格化的人工核查层
"""
from __future__ import annotations
import argparse
import json
import math
import sqlite3
from pathlib import Path
from shapely.geometry import Point, Polygon, mapping
RADIUS = 6378137.0
def mercator_to_lonlat(x: float, y: float) -> tuple[float, float]:
lon = math.degrees(x / RADIUS)
lat = math.degrees(2.0 * math.atan(math.exp(y / RADIUS)) - math.pi / 2.0)
return lon, lat
def polygon_to_lonlat(poly: Polygon) -> Polygon:
exterior = [mercator_to_lonlat(x, y) for x, y in poly.exterior.coords]
interiors = [
[mercator_to_lonlat(x, y) for x, y in ring.coords]
for ring in poly.interiors
]
return Polygon(exterior, interiors)
def point_to_lonlat(point: Point) -> Point:
lon, lat = mercator_to_lonlat(point.x, point.y)
return Point(lon, lat)
def normalize_category(canonical_type: str) -> str:
if canonical_type == "漁港":
return "fishing_port"
if canonical_type == "一般港湾 (港則法区域、重要港湾)":
return "general_harbor"
if canonical_type == "小港湾 (避難港、地方港湾)":
return "small_harbor"
return "unknown"
def style_color(category: str) -> str:
if category == "fishing_port":
return "#7fc8ff"
if category == "general_harbor":
return "#ffb86b"
if category == "small_harbor":
return "#b8d37a"
return "#d9d9d9"
def main() -> None:
parser = argparse.ArgumentParser(description="Export Kyushu port overlay assets")
parser.add_argument(
"--source-db",
default="out/navsea_kyushu_navigability_full_run.export_snapshot.sqlite",
help="输入 SQLite 快照",
)
parser.add_argument(
"--output-dir",
default="src/pbf/port-overlay/kyushu",
help="输出目录",
)
parser.add_argument(
"--slug",
default="port_overlay_kyushu",
help="输出文件前缀",
)
args = parser.parse_args()
project_root = Path(__file__).resolve().parent.parent
source_db = Path(args.source_db)
if not source_db.is_absolute():
source_db = project_root / source_db
out_dir = Path(args.output_dir)
if not out_dir.is_absolute():
out_dir = project_root / out_dir
out_dir.mkdir(parents=True, exist_ok=True)
if not source_db.exists():
raise SystemExit(f"missing source db: {source_db}")
conn = sqlite3.connect(source_db)
conn.row_factory = sqlite3.Row
rows = conn.execute(
"""
SELECT DISTINCT
aoi_id, feature_id, fid, canonical_object_type, center_x, center_y, radius_m
FROM port_aoi
ORDER BY aoi_id
"""
).fetchall()
if not rows:
raise SystemExit("port_aoi is empty")
radius_m = max(float(row["radius_m"]) for row in rows)
types = {}
minx = miny = float("inf")
maxx = maxy = float("-inf")
area_features = []
center_features = []
for row in rows:
category = normalize_category(row["canonical_object_type"])
color = style_color(category)
center = Point(float(row["center_x"]), float(row["center_y"]))
polygon = center.buffer(float(row["radius_m"]), resolution=16)
minx = min(minx, float(row["center_x"]) - float(row["radius_m"]))
miny = min(miny, float(row["center_y"]) - float(row["radius_m"]))
maxx = max(maxx, float(row["center_x"]) + float(row["radius_m"]))
maxy = max(maxy, float(row["center_y"]) + float(row["radius_m"]))
types.setdefault(row["canonical_object_type"], 0)
types[row["canonical_object_type"]] += 1
area_features.append(
{
"type": "Feature",
"properties": {
"aoi_id": row["aoi_id"],
"feature_id": row["feature_id"],
"fid": row["fid"],
"canonical_object_type": row["canonical_object_type"],
"category": category,
"radius_m": float(row["radius_m"]),
"fill": color,
},
"geometry": mapping(polygon_to_lonlat(polygon)),
}
)
center_features.append(
{
"type": "Feature",
"properties": {
"aoi_id": row["aoi_id"],
"feature_id": row["feature_id"],
"fid": row["fid"],
"canonical_object_type": row["canonical_object_type"],
"category": category,
"radius_m": float(row["radius_m"]),
},
"geometry": mapping(point_to_lonlat(center)),
}
)
center_lonlat_min = mercator_to_lonlat(minx, miny)
center_lonlat_max = mercator_to_lonlat(maxx, maxy)
area_geojson = {
"type": "FeatureCollection",
"name": "port_overlay_areas",
"features": area_features,
}
center_geojson = {
"type": "FeatureCollection",
"name": "port_overlay_centers",
"features": center_features,
}
manifest = {
"version": "v1.0",
"source_db": str(source_db),
"source_scope": "Kyushu",
"total_features": len(rows),
"radius_m": radius_m,
"counts_by_type": types,
"bbox_mercator": [minx, miny, maxx, maxy],
"bbox_lonlat": [
center_lonlat_min[0],
center_lonlat_min[1],
center_lonlat_max[0],
center_lonlat_max[1],
],
"area_file": f"{args.slug}_areas.geojson",
"center_file": f"{args.slug}_centers.geojson",
}
(out_dir / f"{args.slug}_areas.geojson").write_text(
json.dumps(area_geojson, ensure_ascii=False),
encoding="utf-8",
)
(out_dir / f"{args.slug}_centers.geojson").write_text(
json.dumps(center_geojson, ensure_ascii=False),
encoding="utf-8",
)
(out_dir / f"{args.slug}_manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2),
encoding="utf-8",
)
conn.close()
print("已导出:")
print(" -", out_dir / f"{args.slug}_areas.geojson")
print(" -", out_dir / f"{args.slug}_centers.geojson")
print(" -", out_dir / f"{args.slug}_manifest.json")
if __name__ == "__main__":
main()