Files
pbf/coastline/export_fish_port_debug_assets.py

519 lines
17 KiB
Python

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