Files
pbf/coastline/export_official_port_overlay_kyushu_assets.py

317 lines
11 KiB
Python

#!/usr/bin/env python3
"""
NavSea official port overlay export v1.0
从官方港湾 / 漁港原始包导出可视化叠加资产:
- C02-08.zip: 港湾
- C09-06.zip: 漁港
默认裁切到九州样区,便于人工核查。
"""
from __future__ import annotations
import argparse
import json
import math
import sqlite3 # kept for symmetry with other exporter scripts; not used directly
import zipfile
from dataclasses import dataclass
from pathlib import Path
import xml.etree.ElementTree as ET
from shapely.geometry import LineString, Point, mapping
RADIUS = 6378137.0
DEFAULT_BBOX = (123.0, 31.0, 132.7, 34.9) # Kyushu preview window
@dataclass(frozen=True)
class SourceSpec:
path: Path
dataset: str
kind: str
def mercator_x(lon: float) -> float:
return RADIUS * math.radians(lon)
def mercator_y(lat: float) -> float:
lat = max(min(lat, 85.0511287798066), -85.0511287798066)
return RADIUS * math.log(math.tan(math.pi / 4.0 + math.radians(lat) / 2.0))
def mercator(lon: float, lat: float) -> tuple[float, float]:
return mercator_x(lon), mercator_y(lat)
def lonlat_from_text(text: str) -> tuple[float, float]:
parts = [p for p in text.replace(",", " ").split() if p]
if len(parts) < 2:
raise ValueError(f"invalid coordinate text: {text!r}")
lat = float(parts[0])
lon = float(parts[1])
return lon, lat
def lonlat_to_lonlat_text(text: str) -> tuple[float, float]:
# helper kept for readability when parsing GML/JPS coordinates
return lonlat_from_text(text)
def local_name(tag: str) -> str:
return tag.split("}", 1)[-1]
def load_xml(zip_path: Path) -> ET.Element:
with zipfile.ZipFile(zip_path) as zf:
xml_name = next(name for name in zf.namelist() if name.endswith(".xml") and "META" not in name)
return ET.fromstring(zf.read(xml_name))
def build_point_lookup(root: ET.Element) -> dict[str, tuple[float, float]]:
lookup: dict[str, tuple[float, float]] = {}
for point in root.iter():
if local_name(point.tag) != "GM_Point":
continue
point_id = point.attrib.get("id")
if not point_id:
continue
coord_text = None
for el in point.iter():
tag = local_name(el.tag)
if tag == "DirectPosition.coordinate" and (el.text or "").strip():
coord_text = el.text.strip()
break
if coord_text is None:
continue
lon, lat = lonlat_from_text(coord_text)
lookup[point_id] = (lon, lat)
return lookup
def extract_curve_coords(curve: ET.Element, point_lookup: dict[str, tuple[float, float]]) -> list[tuple[float, float]]:
coords: list[tuple[float, float]] = []
for pos in curve.iter():
tag = local_name(pos.tag)
if not tag.startswith("GM_Position"):
continue
direct = None
ref = None
for child in list(pos):
child_tag = local_name(child.tag)
if child_tag.endswith("DirectPosition.coordinate") and (child.text or "").strip():
direct = child.text.strip()
break
if child_tag.endswith("PointRef.point"):
ref = child.attrib.get("idref")
if direct is not None:
lon, lat = lonlat_from_text(direct)
coords.append((lon, lat))
elif ref and ref in point_lookup:
coords.append(point_lookup[ref])
return coords
def bbox_intersects(a: tuple[float, float, float, float], b: tuple[float, float, float, float]) -> bool:
return not (a[2] < b[0] or a[0] > b[2] or a[3] < b[1] or a[1] > b[3])
def point_in_bbox(pt: tuple[float, float], bbox: tuple[float, float, float, float]) -> bool:
lon, lat = pt
return bbox[0] <= lon <= bbox[2] and bbox[1] <= lat <= bbox[3]
def normalize_harbor_feature(tag: str, props: dict[str, str]) -> dict[str, str]:
if tag == "CK01":
props["feature_family"] = "harbor_point"
elif tag in ("CK11", "CK21"):
props["feature_family"] = "harbor_line"
elif tag == "CB01":
props["feature_family"] = "fish_port_point"
elif tag == "CB03":
props["feature_family"] = "fish_port_line"
else:
props["feature_family"] = "unknown"
return props
def main() -> None:
parser = argparse.ArgumentParser(description="Export official harbor/fishing-port preview assets for Kyushu")
parser.add_argument(
"--output-dir",
default="src/pbf/port-overlay-official/kyushu",
help="输出目录",
)
parser.add_argument(
"--slug",
default="official_port_overlay_kyushu",
help="输出文件前缀",
)
parser.add_argument(
"--bbox",
nargs=4,
type=float,
default=list(DEFAULT_BBOX),
metavar=("MIN_LON", "MIN_LAT", "MAX_LON", "MAX_LAT"),
help="预览裁切范围,默认九州",
)
args = parser.parse_args()
project_root = Path(__file__).resolve().parent.parent
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)
bbox = tuple(args.bbox) # type: ignore[assignment]
sources = [
SourceSpec(project_root / "coastline" / "C02-08.zip", "C02-08", "harbor"),
SourceSpec(project_root / "coastline" / "C09-06.zip", "C09-06", "fish_port"),
]
line_features: list[dict] = []
point_features: list[dict] = []
stats = []
for source in sources:
if not source.path.exists():
raise SystemExit(f"missing input: {source.path}")
root = load_xml(source.path)
point_lookup = build_point_lookup(root)
obj = root.find(".//{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}OBJ")
if obj is None:
raise SystemExit(f"OBJ block missing in {source.path}")
line_count = 0
point_count = 0
bbox_minx = bbox_miny = float("inf")
bbox_maxx = bbox_maxy = float("-inf")
tags = ["CK11", "CK21"] if source.dataset == "C02-08" else ["CB03"]
point_tags = ["CK01"] if source.dataset == "C02-08" else ["CB01"]
for tag in tags:
for feature in obj.findall(f".//{{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}}{tag}"):
loc = feature.find("{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}LOC")
if loc is None:
continue
ref = loc.attrib.get("idref")
if not ref:
continue
curve = obj.find(f".//*[@id='{ref}']")
if curve is None:
continue
coords = extract_curve_coords(curve, point_lookup)
if len(coords) < 2:
continue
xs = [p[0] for p in coords]
ys = [p[1] for p in coords]
geom_bbox = (min(xs), min(ys), max(xs), max(ys))
if not bbox_intersects(geom_bbox, bbox):
continue
props = {"source_dataset": source.dataset, "source_kind": source.kind, "tag": tag}
for key in ("PRC", "HBC", "FPC"):
el = feature.find(f"{{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}}{key}")
if el is not None and (el.text or "").strip():
props[key.lower()] = el.text.strip()
props = normalize_harbor_feature(tag, props)
if len(coords) == 1:
lon, lat = coords[0]
geometry = mapping(Point(lon, lat))
else:
geometry = mapping(LineString(coords))
line_features.append({"type": "Feature", "properties": props, "geometry": geometry})
line_count += 1
bbox_minx = min(bbox_minx, geom_bbox[0])
bbox_miny = min(bbox_miny, geom_bbox[1])
bbox_maxx = max(bbox_maxx, geom_bbox[2])
bbox_maxy = max(bbox_maxy, geom_bbox[3])
for tag in point_tags:
for feature in obj.findall(f".//{{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}}{tag}"):
pos = feature.find("{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}POS")
if pos is None:
continue
ref = pos.attrib.get("idref")
if not ref or ref not in point_lookup:
continue
lon, lat = point_lookup[ref]
if not point_in_bbox((lon, lat), bbox):
continue
props = {"source_dataset": source.dataset, "source_kind": source.kind, "tag": tag}
for key in ("NA4", "AD7", "NA2", "FCF", "AAC", "CH1", "CH2", "CFP", "FPA", "PRC", "HBC", "FPC"):
el = feature.find(f"{{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}}{key}")
if el is not None and (el.text or "").strip():
props[key.lower()] = el.text.strip()
props = normalize_harbor_feature(tag, props)
point_features.append(
{
"type": "Feature",
"properties": props,
"geometry": mapping(Point(lon, lat)),
}
)
point_count += 1
bbox_minx = min(bbox_minx, lon)
bbox_miny = min(bbox_miny, lat)
bbox_maxx = max(bbox_maxx, lon)
bbox_maxy = max(bbox_maxy, lat)
stats.append(
{
"dataset": source.dataset,
"kind": source.kind,
"path": str(source.path),
"line_count": line_count,
"point_count": point_count,
"bbox_lonlat": None if line_count + point_count == 0 else [bbox_minx, bbox_miny, bbox_maxx, bbox_maxy],
}
)
line_geojson = {
"type": "FeatureCollection",
"name": "official_port_overlay_lines",
"features": line_features,
}
point_geojson = {
"type": "FeatureCollection",
"name": "official_port_overlay_points",
"features": point_features,
}
manifest = {
"version": "v1.0",
"source_packages": [str(s.path) for s in sources],
"bbox_lonlat": list(bbox),
"line_count": len(line_features),
"point_count": len(point_features),
"stats": stats,
"line_file": f"{args.slug}_lines.geojson",
"point_file": f"{args.slug}_points.geojson",
}
(out_dir / f"{args.slug}_lines.geojson").write_text(
json.dumps(line_geojson, ensure_ascii=False),
encoding="utf-8",
)
(out_dir / f"{args.slug}_points.geojson").write_text(
json.dumps(point_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",
)
print("已导出:")
print(" -", out_dir / f"{args.slug}_lines.geojson")
print(" -", out_dir / f"{args.slug}_points.geojson")
print(" -", out_dir / f"{args.slug}_manifest.json")
if __name__ == "__main__":
main()