#!/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()