#!/usr/bin/env python3 from __future__ import annotations import argparse import datetime as dt import json import math import sys from pathlib import Path import numpy as np import pymysql from shapely import get_parts, intersects_xy from shapely.geometry import LineString, box from shapely.ops import polygonize, unary_union from shapely.prepared import prep PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from coastline.build_fish_port_20m_full_mysql_resume import ( align_ceil, align_floor, build_point_lookup, cluster_line_indices, collect_fish_lines_for_prc, discover_target_prcs, load_coastlines_for_prc, load_coarse_windows_for_bbox, load_xml, lon_from_mercator, lat_from_mercator, mercator_x, mercator_y, rect_lonlat, ) DB_NAME = "navsea_japan_coast_grid" DB_USER = "root" DB_PASSWORD = "2chi9ks2" DB_HOST = "localhost" DB_SOCKET = "/tmp/mysql.sock" FISH_SRC = "coastline/C09-06.zip" DEFAULT_OUT_DIR = "src/pbf/coastline-mysql/fish_port_debug" DEFAULT_BUFFER_M = 20.0 DEFAULT_COARSE_MARGIN_M = 0.0 COAST_COARSE_LAYER = "coast_200m" CELL_SIZE_M = 20.0 RADIUS = 6378137.0 def mysql_connect(database: str, *, cursorclass=pymysql.cursors.Cursor): kwargs = { "host": DB_HOST, "user": DB_USER, "password": DB_PASSWORD, "database": database, "charset": "utf8mb4", "cursorclass": cursorclass, } if DB_SOCKET: kwargs["unix_socket"] = DB_SOCKET return pymysql.connect(**kwargs) def mercator_to_lonlat_bbox(bbox: tuple[float, float, float, float]) -> tuple[float, float, float, float]: minx, miny, maxx, maxy = bbox return lon_from_mercator(minx), lat_from_mercator(miny), lon_from_mercator(maxx), lat_from_mercator(maxy) def mercator_geom_to_lonlat_geojson(geom) -> dict: geo = geom.__geo_interface__ def convert_coords(coords): if not isinstance(coords, (list, tuple)): return coords if coords and isinstance(coords[0], (int, float)): x = float(coords[0]) y = float(coords[1]) return [float(lon_from_mercator(x)), float(lat_from_mercator(y))] return [convert_coords(item) for item in coords] if geo["type"] == "GeometryCollection": return { "type": "GeometryCollection", "geometries": [mercator_geom_to_lonlat_geojson(part) for part in geom.geoms], } return { "type": geo["type"], "coordinates": convert_coords(geo["coordinates"]), } def lonlat_line_feature( coords: list[tuple[float, float]], *, kind: str, fpc: str, prc: str, label: str | None = None, ) -> dict: properties = { "kind": kind, "fpc": fpc, "prc": prc, } if label is not None: properties["label"] = label return { "type": "Feature", "properties": properties, "geometry": { "type": "LineString", "coordinates": [[float(lon), float(lat)] for lon, lat in coords], }, } def mercator_line_feature( line: LineString, *, kind: str, fpc: str, prc: str, label: str | None = None, ) -> dict: coords = [[float(lon_from_mercator(x)), float(lat_from_mercator(y))] for x, y in line.coords] return { "type": "Feature", "properties": { "kind": kind, "fpc": fpc, "prc": prc, **({"label": label} if label is not None else {}), }, "geometry": { "type": "LineString", "coordinates": coords, }, } def bbox_feature( bbox: tuple[float, float, float, float], *, kind: str, fpc: str, prc: str, label: str, ) -> dict: min_lon, min_lat, max_lon, max_lat = bbox return { "type": "Feature", "properties": { "kind": kind, "fpc": fpc, "prc": prc, "label": label, }, "geometry": rect_lonlat(min_lon, min_lat, max_lon, max_lat), } def mercator_bbox_feature( bbox: tuple[float, float, float, float], *, kind: str, fpc: str, prc: str, label: str, ) -> dict: return bbox_feature( mercator_to_lonlat_bbox(bbox), kind=kind, fpc=fpc, prc=prc, label=label, ) def buffer_tag(value: float) -> str: if abs(value - round(value)) < 1e-9: return str(int(round(value))) return str(value).replace(".", "p") def scan_debug_cells( scan_geom, coast_prepared, *, prc: str, fpc: str, cluster_idx: int, part_idx: int, window_idx: int, max_scan_cells: int | None, ) -> tuple[list[dict], int, int]: fish_prepared = prep(scan_geom) minx, miny, maxx, maxy = scan_geom.bounds start_x = align_floor(minx, CELL_SIZE_M) start_y = align_floor(miny, CELL_SIZE_M) end_x = align_ceil(maxx, CELL_SIZE_M) end_y = align_ceil(maxy, CELL_SIZE_M) estimated_cols = max(0, int(math.ceil((end_x - start_x) / CELL_SIZE_M))) estimated_rows = max(0, int(math.ceil((end_y - start_y) / CELL_SIZE_M))) estimated_cells = estimated_cols * estimated_rows if max_scan_cells is not None and estimated_cells > max_scan_cells: raise RuntimeError( "scan guard triggered: " f"estimated_cells={estimated_cells} max_scan_cells={max_scan_cells} " f"grid={estimated_cols}x{estimated_rows} bounds={tuple(round(v, 3) for v in scan_geom.bounds)} " f"fpc={fpc} prc={prc} cluster={cluster_idx} part={part_idx} window={window_idx}" ) half = CELL_SIZE_M / 2.0 ix_start = int(round(start_x / CELL_SIZE_M)) ix_end = int(round(end_x / CELL_SIZE_M)) ix_values = np.arange(ix_start, ix_end, dtype=np.int64) x_values = ix_values.astype(float) * CELL_SIZE_M x_center_values = x_values + half x_right_values = x_values + CELL_SIZE_M features: list[dict] = [] coast_count = 0 sea_count = 0 y = start_y while y < end_y: row_strip = box(start_x, y, end_x, y + CELL_SIZE_M) if not fish_prepared.intersects(row_strip): y += CELL_SIZE_M continue mask = ( intersects_xy(scan_geom, x_center_values, y + half) | intersects_xy(scan_geom, x_values, y) | intersects_xy(scan_geom, x_right_values, y) | intersects_xy(scan_geom, x_values, y + CELL_SIZE_M) | intersects_xy(scan_geom, x_right_values, y + CELL_SIZE_M) ) if not np.any(mask): y += CELL_SIZE_M continue mask = mask | np.r_[False, mask[:-1]] | np.r_[mask[1:], False] for idx in np.flatnonzero(mask): x = float(x_values[idx]) cell_geom = box(x, y, x + CELL_SIZE_M, y + CELL_SIZE_M) if not fish_prepared.intersects(cell_geom): continue coast_hit = coast_prepared.intersects(cell_geom) min_lon = lon_from_mercator(x) min_lat = lat_from_mercator(y) max_lon = lon_from_mercator(x + CELL_SIZE_M) max_lat = lat_from_mercator(y + CELL_SIZE_M) features.append( { "type": "Feature", "properties": { "kind": "cell_coast" if coast_hit else "cell_sea", "fpc": fpc, "prc": prc, "cluster_idx": int(cluster_idx), "part_idx": int(part_idx), "window_idx": int(window_idx), "cell_id": f"{int(round(x / CELL_SIZE_M))}:{int(round(y / CELL_SIZE_M))}", "row": int(round(y / CELL_SIZE_M)), "col": int(round(x / CELL_SIZE_M)), "cell_size_m": CELL_SIZE_M, "coast_hit": coast_hit, }, "geometry": rect_lonlat(min_lon, min_lat, max_lon, max_lat), } ) if coast_hit: coast_count += 1 else: sea_count += 1 y += CELL_SIZE_M return features, coast_count, sea_count def main() -> None: parser = argparse.ArgumentParser(description="Export one fish port debug bundle") parser.add_argument("--db-name", default=DB_NAME) parser.add_argument("--out-dir", default=DEFAULT_OUT_DIR) parser.add_argument("--fpc", required=True, help="港口 FPC 号,例如 4320050") parser.add_argument("--coast-buffer-m", type=float, default=DEFAULT_BUFFER_M, help="海岸缓冲距离(米)") parser.add_argument( "--coast-coarse-margin-m", type=float, default=DEFAULT_COARSE_MARGIN_M, help="coast_200m 粗窗查询外扩距离(米)", ) parser.add_argument("--cluster-gap-m", type=float, default=250.0, help="线聚类间隔(米)") parser.add_argument("--max-scan-cells", type=int, default=200000, help="单窗允许扫描的最大 20m 格数") args = parser.parse_args() project_root = Path(__file__).resolve().parent.parent out_dir = Path(args.out_dir) if not out_dir.is_absolute(): out_dir = project_root / out_dir fpc = str(args.fpc).strip() buffer_dir = out_dir / f"fpc_{fpc}" / f"buffer_{buffer_tag(args.coast_buffer_m)}" buffer_dir.mkdir(parents=True, exist_ok=True) root = load_xml(project_root / FISH_SRC) point_lookup = build_point_lookup(root) prcs = discover_target_prcs(root, None, fpc) if not prcs: raise SystemExit(f"no PRC matched for FPC={fpc}") prc = prcs[0] fish_lines = collect_fish_lines_for_prc(root, point_lookup, prc, selected_fpc=fpc) if not fish_lines: raise SystemExit(f"no fish-port line features selected for FPC={fpc}") fish_bboxes = [line.bbox for line in fish_lines] fish_bbox = ( min(bb[0] for bb in fish_bboxes), min(bb[1] for bb in fish_bboxes), max(bb[2] for bb in fish_bboxes), max(bb[3] for bb in fish_bboxes), ) coast_lines, coast_bbox = load_coastlines_for_prc( project_root, prc, bbox=( mercator_x(fish_bbox[0]), mercator_y(fish_bbox[1]), mercator_x(fish_bbox[2]), mercator_y(fish_bbox[3]), ), search_margin_m=max(args.coast_buffer_m, args.coast_coarse_margin_m), ) coast_geom = unary_union(coast_lines) coast_buffer = coast_geom.buffer(args.coast_buffer_m, cap_style=2, join_style=2) coast_prepared = prep(coast_buffer) conn = mysql_connect(args.db_name) try: prc_coarse_windows = load_coarse_windows_for_bbox( conn, layer_name=COAST_COARSE_LAYER, bbox_mercator=( mercator_x(fish_bbox[0]), mercator_y(fish_bbox[1]), mercator_x(fish_bbox[2]), mercator_y(fish_bbox[3]), ), margin_m=args.coast_coarse_margin_m, ) cluster_groups = cluster_line_indices([line.bbox for line in fish_lines], gap_m=args.cluster_gap_m) features: list[dict] = [] features.append( bbox_feature( fish_bbox, kind="fish_bbox", fpc=fpc, prc=prc, label="fish_bbox", ) ) features.append( mercator_bbox_feature( coast_bbox, kind="coast_bbox", fpc=fpc, prc=prc, label="coast_bbox", ) ) for line in fish_lines: features.append( lonlat_line_feature( line.coords, kind="fish_line", fpc=fpc, prc=prc, label="fish_line", ) ) for line in coast_lines: features.append( mercator_line_feature( line, kind="coast_line", fpc=fpc, prc=prc, label="coast_line", ) ) features.append( { "type": "Feature", "properties": { "kind": "coast_buffer", "fpc": fpc, "prc": prc, "buffer_m": args.coast_buffer_m, "label": f"buffer={args.coast_buffer_m}m", }, "geometry": mercator_geom_to_lonlat_geojson(coast_buffer), } ) for idx, coarse_window in enumerate(prc_coarse_windows): features.append( bbox_feature( mercator_to_lonlat_bbox(coarse_window), kind="coast_200m_window", fpc=fpc, prc=prc, label=f"coast_200m_window_{idx + 1}", ) ) scan_window_total = 0 cell_coast_total = 0 cell_sea_total = 0 for cluster_idx, cluster in enumerate(cluster_groups): cluster_lines = [ LineString([(mercator_x(lon), mercator_y(lat)) for lon, lat in fish_lines[i].coords]) for i in cluster ] merged = unary_union(cluster_lines) polygons = list(polygonize(merged)) if polygons: fish_geom = unary_union(polygons) else: fish_geom = merged.buffer(CELL_SIZE_M, cap_style=2, join_style=2) fish_parts = [part for part in get_parts(fish_geom) if not part.is_empty] for part_idx, fish_part in enumerate(fish_parts): part_bbox = fish_part.bounds part_coarse_windows = load_coarse_windows_for_bbox( conn, layer_name=COAST_COARSE_LAYER, bbox_mercator=part_bbox, margin_m=args.coast_coarse_margin_m, ) for window_idx, coarse_window in enumerate(part_coarse_windows): window_geom = box(*coarse_window) scan_geom = window_geom scan_window_total += 1 features.append( { "type": "Feature", "properties": { "kind": "scan_window", "fpc": fpc, "prc": prc, "cluster_idx": int(cluster_idx), "part_idx": int(part_idx), "window_idx": int(window_idx), "label": f"scan_window_{cluster_idx + 1}_{part_idx + 1}_{window_idx + 1}", }, "geometry": mercator_geom_to_lonlat_geojson(scan_geom), } ) cells, coast_count, sea_count = scan_debug_cells( scan_geom, coast_prepared, prc=prc, fpc=fpc, cluster_idx=cluster_idx, part_idx=part_idx, window_idx=window_idx, max_scan_cells=args.max_scan_cells, ) features.extend(cells) cell_coast_total += coast_count cell_sea_total += sea_count out_path = buffer_dir / "debug.geojson" out_path.write_text( json.dumps({"type": "FeatureCollection", "features": features}, ensure_ascii=False, indent=2), encoding="utf-8", ) manifest = { "database": args.db_name, "generated_at": dt.datetime.now().isoformat(timespec="seconds"), "fpc": fpc, "prc": prc, "buffer_m": args.coast_buffer_m, "coast_coarse_margin_m": args.coast_coarse_margin_m, "fish_bbox_lonlat": list(fish_bbox), "fish_line_count": len(fish_lines), "coast_curve_count": len(coast_lines), "coast_bbox_mercator": list(coast_bbox), "coast_200m_window_count": len(prc_coarse_windows), "scan_window_count": scan_window_total, "cell_coast_count": cell_coast_total, "cell_sea_count": cell_sea_total, "feature_count": len(features), "export_file": str(out_path.relative_to(project_root)) if project_root in out_path.parents else str(out_path), } (buffer_dir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") print(f"debug: {len(features)} -> {out_path}") print(f"manifest: {buffer_dir / 'manifest.json'}") finally: conn.close() if __name__ == "__main__": main()