docs(pbf): archive Full production provenance and engineering history
This commit is contained in:
219
coastline/export_port_overlay_kyushu_assets.py
Normal file
219
coastline/export_port_overlay_kyushu_assets.py
Normal 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()
|
||||
Reference in New Issue
Block a user