220 lines
6.5 KiB
Python
220 lines
6.5 KiB
Python
#!/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()
|