Files
pbf/coastline/build_fish_port_20m_full_mysql_resume.py
2026-05-02 14:32:06 +08:00

1824 lines
76 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import datetime as dt
import json
import math
import time
import zipfile
import xml.etree.ElementTree as ET
from functools import wraps
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
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
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"
COAST_DIR = Path("coastline")
LAYER_NAME = "fish_port_20m"
CELL_SIZE_M = 20.0
COAST_BUFFER_M = 12.0
COAST_COARSE_LAYER = "coast_200m"
DEFAULT_COAST_COARSE_MARGIN_M = 0.0
DEFAULT_CHECKPOINT = "out/fish_port_20m_full_resume.checkpoint.json"
DEFAULT_CURRENT_PRC_FILE = "out/fish_port_20m_full_resume.current_prc.json"
RESUME_JOB_NAME = "fish_port_20m_full_mysql_resume"
RADIUS = 6378137.0
MAX_MERCATOR_LAT = 85.0511287798066
@dataclass(frozen=True)
class FishLine:
prc: str
fpc: str | None
coords: list[tuple[float, float]]
bbox: tuple[float, float, float, float]
def mercator_x(lon: float) -> float:
return RADIUS * math.radians(lon)
def mercator_y(lat: float) -> float:
lat = max(min(lat, MAX_MERCATOR_LAT), -MAX_MERCATOR_LAT)
return RADIUS * math.log(math.tan(math.pi / 4.0 + math.radians(lat) / 2.0))
def lon_from_mercator(x: float) -> float:
return math.degrees(x / RADIUS)
def lat_from_mercator(y: float) -> float:
return math.degrees(2.0 * math.atan(math.exp(y / RADIUS)) - math.pi / 2.0)
def local_name(tag: str) -> str:
return tag.split("}", 1)[-1]
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 log(message: str) -> None:
print(f"[{dt.datetime.now().isoformat(timespec='seconds')}] {message}")
def summarize_value(value):
if isinstance(value, Path):
return str(value)
if isinstance(value, ET.Element):
return f"<Element {local_name(value.tag)}>"
if isinstance(value, dict):
return f"dict(len={len(value)})"
if isinstance(value, (list, tuple, set)):
return f"{type(value).__name__}(len={len(value)})"
if hasattr(value, "geom_type"):
try:
bounds = tuple(round(v, 3) for v in value.bounds)
except Exception:
bounds = None
return f"{getattr(value, 'geom_type', type(value).__name__)}(bounds={bounds})"
return repr(value)
def trace_fn(label: str | None = None):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
name = label or func.__name__
arg_bits = [summarize_value(arg) for arg in args]
kw_bits = [f"{key}={summarize_value(val)}" for key, val in kwargs.items()]
joined = ", ".join(arg_bits + kw_bits)
start = time.monotonic()
log(f"[TRACE] {name} 开始 {joined}")
try:
result = func(*args, **kwargs)
except Exception as exc:
log(f"[TRACE] {name} 异常 {type(exc).__name__}: {exc}")
raise
elapsed = time.monotonic() - start
log(f"[TRACE] {name} 完成 elapsed={elapsed:.2f}s")
return result
return wrapper
return decorator
@trace_fn("load_xml")
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():
if local_name(el.tag).endswith("DirectPosition.coordinate") and (el.text or "").strip():
coord_text = el.text.strip()
break
if coord_text is None:
continue
lookup[point_id] = lonlat_from_text(coord_text)
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:
coords.append(lonlat_from_text(direct))
elif ref and ref in point_lookup:
coords.append(point_lookup[ref])
return coords
def bbox_from_coords(coords: list[tuple[float, float]]) -> tuple[float, float, float, float]:
xs = [p[0] for p in coords]
ys = [p[1] for p in coords]
return min(xs), min(ys), max(xs), max(ys)
def align_floor(value: float, step: float) -> float:
return math.floor(value / step) * step
def align_ceil(value: float, step: float) -> float:
return math.ceil(value / step) * step
def expand_bbox(bbox: tuple[float, float, float, float], margin_m: float) -> tuple[float, float, float, float]:
return (bbox[0] - margin_m, bbox[1] - margin_m, bbox[2] + margin_m, bbox[3] + margin_m)
def mysql_connect(database: str | None = None):
kwargs = {
"host": DB_HOST,
"user": DB_USER,
"password": DB_PASSWORD,
"charset": "utf8mb4",
"autocommit": False,
"cursorclass": pymysql.cursors.Cursor,
}
if DB_SOCKET:
kwargs["unix_socket"] = DB_SOCKET
if database:
kwargs["database"] = database
return pymysql.connect(**kwargs)
def ensure_database() -> None:
conn = mysql_connect()
try:
with conn.cursor() as cur:
cur.execute(
f"CREATE DATABASE IF NOT EXISTS `{DB_NAME}` "
"DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
)
conn.commit()
finally:
conn.close()
def ensure_schema(conn) -> None:
stmts = [
"""
CREATE TABLE IF NOT EXISTS navsea_grid_import_state (
job_name VARCHAR(64) NOT NULL PRIMARY KEY,
layer_name VARCHAR(32) NOT NULL,
source_name VARCHAR(191) NOT NULL,
checkpoint_json LONGTEXT NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""",
"""
CREATE TABLE IF NOT EXISTS navsea_grid_import_progress (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
job_name VARCHAR(64) NOT NULL,
layer_name VARCHAR(32) NOT NULL,
prc VARCHAR(8) NOT NULL,
status_name VARCHAR(32) NOT NULL,
cluster_count INT NOT NULL,
inserted_cells BIGINT NOT NULL,
land_cells BIGINT NOT NULL,
sea_cells BIGINT NOT NULL,
elapsed_sec DOUBLE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_job_layer_prc (job_name, layer_name, prc),
KEY idx_job_created (job_name, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""",
"""
CREATE TABLE IF NOT EXISTS navsea_grid_layer_meta (
layer_name VARCHAR(32) NOT NULL PRIMARY KEY,
description VARCHAR(255) NOT NULL,
source_desc TEXT NOT NULL,
cell_size_m DOUBLE NOT NULL,
feature_count BIGINT NOT NULL,
bbox_min_lon DOUBLE NOT NULL,
bbox_min_lat DOUBLE NOT NULL,
bbox_max_lon DOUBLE NOT NULL,
bbox_max_lat DOUBLE NOT NULL,
export_file VARCHAR(255) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""",
"""
CREATE TABLE IF NOT EXISTS navsea_grid_cell (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
layer_name VARCHAR(32) NOT NULL,
cell_id VARCHAR(64) NOT NULL,
row_idx INT NOT NULL,
col_idx INT NOT NULL,
cell_size_m DOUBLE NOT NULL,
state_name VARCHAR(32) NOT NULL,
class_name VARCHAR(32) NOT NULL,
source_name VARCHAR(191) NOT NULL,
min_lon DOUBLE NOT NULL,
min_lat DOUBLE NOT NULL,
max_lon DOUBLE NOT NULL,
max_lat DOUBLE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_layer_cell (layer_name, cell_id),
KEY idx_layer_state (layer_name, state_name),
KEY idx_layer_rowcol (layer_name, row_idx, col_idx)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""",
]
with conn.cursor() as cur:
for stmt in stmts:
cur.execute(stmt)
conn.commit()
def upsert_meta(
cur,
layer_name: str,
description: str,
source_desc: str,
cell_size_m: float,
feature_count: int,
bbox: tuple[float, float, float, float],
export_file: str | None,
) -> None:
cur.execute(
"""
INSERT INTO navsea_grid_layer_meta
(layer_name, description, source_desc, cell_size_m, feature_count,
bbox_min_lon, bbox_min_lat, bbox_max_lon, bbox_max_lat, export_file)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
description=VALUES(description),
source_desc=VALUES(source_desc),
cell_size_m=VALUES(cell_size_m),
feature_count=VALUES(feature_count),
bbox_min_lon=VALUES(bbox_min_lon),
bbox_min_lat=VALUES(bbox_min_lat),
bbox_max_lon=VALUES(bbox_max_lon),
bbox_max_lat=VALUES(bbox_max_lat),
export_file=VALUES(export_file)
""",
(
layer_name,
description,
source_desc,
cell_size_m,
feature_count,
bbox[0],
bbox[1],
bbox[2],
bbox[3],
export_file,
),
)
def insert_cells(cur, rows: list[tuple]) -> None:
if not rows:
return
cur.executemany(
"""
INSERT IGNORE INTO navsea_grid_cell
(layer_name, cell_id, row_idx, col_idx, cell_size_m, state_name, class_name, source_name,
min_lon, min_lat, max_lon, max_lat)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
rows,
)
def save_checkpoint(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = path.with_suffix(path.suffix + ".tmp")
tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
tmp_path.replace(path)
def load_checkpoint(path: Path) -> dict | None:
if not path.exists():
return None
return json.loads(path.read_text(encoding="utf-8"))
def normalize_prc_list(values: Iterable[str]) -> list[str]:
unique = {normalize_prc_token(str(value)) for value in values}
try:
return sorted(unique, key=lambda item: int(item))
except ValueError:
return sorted(unique)
def normalize_prc_token(value: str) -> str:
value = str(value).strip()
if value.isdigit():
return f"{int(value):02d}"
return value
def normalize_fpc_token(value: str) -> str:
return str(value).strip()
def load_resume_state_from_db(conn, job_name: str, layer_name: str) -> dict | None:
with conn.cursor() as cur:
cur.execute(
"""
SELECT checkpoint_json
FROM navsea_grid_import_state
WHERE job_name=%s AND layer_name=%s
""",
(job_name, layer_name),
)
row = cur.fetchone()
if not row or row[0] is None:
return None
state = json.loads(row[0])
cur.execute(
"""
SELECT prc
FROM navsea_grid_import_progress
WHERE job_name=%s AND layer_name=%s AND status_name='PRC_DONE'
ORDER BY id
""",
(job_name, layer_name),
)
completed_prcs = [str(prc) for (prc,) in cur.fetchall()]
completed_prcs = normalize_prc_list(completed_prcs)
state["completed_prcs"] = completed_prcs
state.setdefault("job_name", job_name)
state.setdefault("layer_name", layer_name)
state.setdefault("status", "running")
return state
def load_resume_state(conn, _path: Path, job_name: str, layer_name: str) -> dict | None:
state = load_resume_state_from_db(conn, job_name, layer_name)
if state is not None:
return state
return None
def load_coarse_mask(conn, layer_name: str) -> tuple[object | None, tuple[float, float, float, float] | None, int]:
with conn.cursor() as cur:
cur.execute(
"""
SELECT min_lon, min_lat, max_lon, max_lat
FROM navsea_grid_cell
WHERE layer_name=%s
""",
(layer_name,),
)
boxes = [box(float(min_lon), float(min_lat), float(max_lon), float(max_lat)) for min_lon, min_lat, max_lon, max_lat in cur]
if not boxes:
return None, None, 0
geom = unary_union(boxes)
return geom, geom.bounds, len(boxes)
def load_coarse_windows_for_bbox(
conn,
*,
layer_name: str,
bbox_mercator: tuple[float, float, float, float],
margin_m: float,
) -> list[tuple[float, float, float, float]]:
minx, miny, maxx, maxy = bbox_mercator
query_minx = minx - margin_m
query_miny = miny - margin_m
query_maxx = maxx + margin_m
query_maxy = maxy + margin_m
with conn.cursor() as cur:
cur.execute(
"""
SELECT min_lon, min_lat, max_lon, max_lat
FROM navsea_grid_cell
WHERE layer_name=%s
AND NOT (
max_lon < %s OR min_lon > %s OR
max_lat < %s OR min_lat > %s
)
ORDER BY row_idx, col_idx
""",
(layer_name, query_minx, query_maxx, query_miny, query_maxy),
)
windows = [
(
float(row_min_lon),
float(row_min_lat),
float(row_max_lon),
float(row_max_lat),
)
for row_min_lon, row_min_lat, row_max_lon, row_max_lat in cur
]
log(
f"[TRACE] load_coarse_windows_for_bbox 完成 layer={layer_name} "
f"bbox={tuple(round(v, 3) for v in bbox_mercator)} margin={margin_m:.1f}m "
f"windows={len(windows)}"
)
return windows
def delete_cells_by_ids(conn, layer_name: str, cell_ids: list[str]) -> None:
if not cell_ids:
return
with conn.cursor() as cur:
for offset in range(0, len(cell_ids), 1000):
chunk = cell_ids[offset : offset + 1000]
placeholders = ", ".join(["%s"] * len(chunk))
cur.execute(
f"DELETE FROM navsea_grid_cell WHERE layer_name=%s AND cell_id IN ({placeholders})",
[layer_name, *chunk],
)
conn.commit()
def drop_existing_layer(conn, layer_name: str) -> None:
with conn.cursor() as cur:
cur.execute("DELETE FROM navsea_grid_cell WHERE layer_name=%s", (layer_name,))
cur.execute("DELETE FROM navsea_grid_layer_meta WHERE layer_name=%s", (layer_name,))
cur.execute("DELETE FROM navsea_grid_import_state WHERE job_name=%s", (RESUME_JOB_NAME,))
cur.execute("DELETE FROM navsea_grid_import_progress WHERE job_name=%s", (RESUME_JOB_NAME,))
conn.commit()
def drop_existing_prcs(conn, layer_name: str, prcs: set[str]) -> None:
if not prcs:
return
source_names = [f"C09-06+coastline PRC={prc}" for prc in sorted(normalize_prc_list(prcs))]
placeholders = ", ".join(["%s"] * len(source_names))
with conn.cursor() as cur:
cur.execute(
f"DELETE FROM navsea_grid_cell WHERE layer_name=%s AND source_name IN ({placeholders})",
[layer_name, *source_names],
)
cur.execute(
f"DELETE FROM navsea_grid_import_progress WHERE job_name=%s AND prc IN ({placeholders})",
[RESUME_JOB_NAME, *sorted(prcs)],
)
conn.commit()
def write_prc_status(path: Path, payload: dict) -> None:
save_checkpoint(path, payload)
def insert_progress(
cur,
*,
job_name: str,
layer_name: str,
prc: str,
status_name: str,
cluster_count: int,
inserted_cells: int,
land_cells: int,
sea_cells: int,
elapsed_sec: float,
) -> None:
cur.execute(
"""
INSERT INTO navsea_grid_import_progress
(job_name, layer_name, prc, status_name, cluster_count, inserted_cells, land_cells, sea_cells, elapsed_sec)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
job_name,
layer_name,
prc,
status_name,
cluster_count,
inserted_cells,
land_cells,
sea_cells,
elapsed_sec,
),
)
def rect_lonlat(min_lon: float, min_lat: float, max_lon: float, max_lat: float) -> dict:
return {
"type": "Polygon",
"coordinates": [
[
[min_lon, min_lat],
[max_lon, min_lat],
[max_lon, max_lat],
[min_lon, max_lat],
[min_lon, min_lat],
]
],
}
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])
@trace_fn("discover_prcs")
def discover_prcs(root: ET.Element, selected_prc: set[str] | None) -> list[str]:
obj = root.find(".//{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}OBJ")
if obj is None:
raise SystemExit("OBJ block missing in C09-06.zip")
prcs: set[str] = set()
for feature in obj.findall(".//{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}CB03"):
prc_el = feature.find("{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}PRC")
prc = normalize_prc_token(prc_el.text) if prc_el is not None and (prc_el.text or "").strip() else "__unknown__"
if selected_prc is not None and prc not in selected_prc:
continue
prcs.add(prc)
return sorted(prcs)
@trace_fn("discover_target_prcs")
def discover_target_prcs(
root: ET.Element,
selected_prc: set[str] | None,
selected_fpc: str | None,
) -> list[str]:
if selected_fpc is None:
return discover_prcs(root, selected_prc)
obj = root.find(".//{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}OBJ")
if obj is None:
raise SystemExit("OBJ block missing in C09-06.zip")
prcs: set[str] = set()
for feature in obj.findall(".//{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}CB03"):
prc_el = feature.find("{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}PRC")
fpc_el = feature.find("{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}FPC")
prc = normalize_prc_token(prc_el.text) if prc_el is not None and (prc_el.text or "").strip() else "__unknown__"
fpc = normalize_fpc_token(fpc_el.text) if fpc_el is not None and (fpc_el.text or "").strip() else None
if selected_prc is not None and prc not in selected_prc:
continue
if fpc != selected_fpc:
continue
prcs.add(prc)
return sorted(prcs)
@trace_fn("collect_fish_lines_for_prc")
def collect_fish_lines_for_prc(
root: ET.Element,
point_lookup: dict[str, tuple[float, float]],
selected_prc: str,
selected_fpc: str | None = None,
) -> list[FishLine]:
obj = root.find(".//{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}OBJ")
if obj is None:
raise SystemExit("OBJ block missing in C09-06.zip")
fish_lines: list[FishLine] = []
for feature in obj.findall(".//{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}CB03"):
prc_el = feature.find("{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}PRC")
fpc_el = feature.find("{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}FPC")
prc = normalize_prc_token(prc_el.text) if prc_el is not None and (prc_el.text or "").strip() else "__unknown__"
if prc != selected_prc:
continue
fpc = normalize_fpc_token(fpc_el.text) if fpc_el is not None and (fpc_el.text or "").strip() else None
if selected_fpc is not None and fpc != selected_fpc:
continue
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
bbox = bbox_from_coords(coords)
fish_lines.append(FishLine(prc=prc, fpc=fpc, coords=coords, bbox=bbox))
return fish_lines
@trace_fn("cluster_line_indices")
def cluster_line_indices(bboxes: list[tuple[float, float, float, float]], gap_m: float = 250.0) -> list[list[int]]:
parent = list(range(len(bboxes)))
def find(x: int) -> int:
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a: int, b: int) -> None:
ra = find(a)
rb = find(b)
if ra != rb:
parent[rb] = ra
expanded = [
(bb[0] - gap_m, bb[1] - gap_m, bb[2] + gap_m, bb[3] + gap_m)
for bb in bboxes
]
for i in range(len(expanded)):
for j in range(i + 1, len(expanded)):
if bbox_intersects(expanded[i], expanded[j]):
union(i, j)
groups: dict[int, list[int]] = {}
for idx in range(len(parent)):
root = find(idx)
groups.setdefault(root, []).append(idx)
return list(groups.values())
@trace_fn("load_coastlines_for_prc")
def load_coastlines_for_prc(
project_root: Path,
prc: str,
bbox: tuple[float, float, float, float] | None = None,
search_margin_m: float = 250.0,
) -> tuple[list[LineString], tuple[float, float, float, float]]:
bbox_geom = box(*expand_bbox(bbox, search_margin_m)) if bbox is not None else None
lines: list[LineString] = []
bbox = [float("inf"), float("inf"), float("-inf"), float("-inf")]
coast_dir = project_root / COAST_DIR
exact_path = coast_dir / f"C23-06_{prc}_GML.zip"
candidate_paths = [exact_path] if exact_path.exists() else []
fallback_paths = sorted(coast_dir.glob("C23-06_*_GML.zip"))
if exact_path.exists():
log(f"[TRACE] load_coastlines_for_prc 使用同号海岸线包 {exact_path.name}")
elif fallback_paths:
log(
f"[TRACE] load_coastlines_for_prc 缺少同号海岸线包 {exact_path.name}"
f"将按 bbox 退回扫描 {len(fallback_paths)} 个现有海岸线包"
)
candidate_paths = fallback_paths
else:
raise FileNotFoundError(str(exact_path))
tried_paths: list[Path] = []
def scan_path(coast_path: Path) -> None:
root = load_xml(coast_path)
point_lookup = build_point_lookup(root)
found_here = 0
for curve in root.findall(".//{http://www.opengis.net/gml/3.2}Curve"):
coords: list[tuple[float, float]] = []
for pos_list in curve.findall(".//{http://www.opengis.net/gml/3.2}posList"):
if not pos_list.text:
continue
values = [float(part) for part in pos_list.text.split() if part]
if len(values) < 4 or len(values) % 2 != 0:
continue
for i in range(0, len(values), 2):
lat = values[i]
lon = values[i + 1]
coords.append((mercator_x(lon), mercator_y(lat)))
if len(coords) < 2:
coords = [(mercator_x(lon), mercator_y(lat)) for lon, lat in extract_curve_coords(curve, point_lookup)]
if len(coords) < 2:
continue
line = LineString(coords)
if bbox_geom is not None and not line.intersects(bbox_geom):
continue
lines.append(line)
found_here += 1
xs = [p[0] for p in coords]
ys = [p[1] for p in coords]
bbox[0] = min(bbox[0], min(xs))
bbox[1] = min(bbox[1], min(ys))
bbox[2] = max(bbox[2], max(xs))
bbox[3] = max(bbox[3], max(ys))
tried_paths.append(coast_path)
if found_here > 0:
log(
f"[TRACE] load_coastlines_for_prc 海岸线命中 source={coast_path.name} "
f"hits={found_here}"
)
for coast_path in candidate_paths:
scan_path(coast_path)
if not lines and exact_path.exists():
other_paths = [p for p in fallback_paths if p != exact_path]
if other_paths:
log(
f"[TRACE] load_coastlines_for_prc 同号包 {exact_path.name} 未命中 bbox"
f"改用其他 {len(other_paths)} 个海岸线包继续搜索"
)
for coast_path in other_paths:
scan_path(coast_path)
if not lines:
raise SystemExit(
f"no coastline curves parsed for PRC={prc}; "
f"tried={len(tried_paths)} bbox={tuple(round(v, 3) for v in bbox_geom.bounds) if bbox_geom is not None else None}"
)
return lines, (bbox[0], bbox[1], bbox[2], bbox[3])
@trace_fn("build_cells_for_geometry")
def build_cells_for_geometry(
fish_geom,
coast_prepared,
*,
prc: str,
cell_size_m: float,
scan_tile_m: float,
source_name: str,
max_scan_cells: int | None,
) -> tuple[list[tuple], int, int, tuple[float, float, float, float], dict[str, float | int]]:
fish_prepared = prep(fish_geom)
minx, miny, maxx, maxy = fish_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 fish_geom.bounds)} "
f"source={source_name} prc={prc}"
)
log(
f"[TRACE] build_cells_for_geometry 准备 "
f"fish_bounds={tuple(round(v, 3) for v in fish_geom.bounds)} "
f"scan_range=({start_x:.3f}, {start_y:.3f}, {end_x:.3f}, {end_y:.3f}) "
f"grid={estimated_cols}x{estimated_rows} estimated_cells={estimated_cells} "
f"cell={cell_size_m:.1f}m tile={scan_tile_m:.1f}m source={source_name}"
)
rows: list[tuple] = []
land_count = 0
sea_count = 0
export_bbox = [float("inf"), float("inf"), float("-inf"), float("-inf")]
profile = {
"tile_checked": 0,
"tile_hit": 0,
"row_checked": 0,
"row_hit": 0,
"mask_row_hit": 0,
"candidate_cells": 0,
"precise_cell_checks": 0,
"precise_hits": 0,
}
tile_start_x = align_floor(start_x, scan_tile_m)
tile_start_y = align_floor(start_y, scan_tile_m)
tile_end_x = align_ceil(end_x, scan_tile_m)
tile_end_y = align_ceil(end_y, scan_tile_m)
half = cell_size_m / 2.0
log(
f"[TRACE] build_cells_for_geometry tile_range="
f"({tile_start_x:.3f}, {tile_start_y:.3f}, {tile_end_x:.3f}, {tile_end_y:.3f})"
)
# 对 200m 粗窗这类很小的输入,直接按 20m 网格线性扫描,
# 避免再走一层 2000m tile 外循环。
if (end_x - start_x) <= scan_tile_m and (end_y - start_y) <= scan_tile_m:
profile["tile_checked"] = 1
profile["tile_hit"] = 1
log(f"[TRACE] build_cells_for_geometry 使用小窗直算模式")
y = start_y
while y < end_y:
profile["row_checked"] += 1
row_strip = box(start_x, y, end_x, y + cell_size_m)
if not fish_prepared.intersects(row_strip):
y += cell_size_m
continue
profile["row_hit"] += 1
if profile["row_hit"] <= 20 or profile["row_hit"] % 50 == 0:
log(
f"[TRACE] build_cells_for_geometry row_hit "
f"row_idx={profile['row_checked']} y={y:.3f} x_span=({start_x:.3f}, {end_x:.3f})"
)
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
mask = (
intersects_xy(fish_geom, x_center_values, y + half)
| intersects_xy(fish_geom, x_values, y)
| intersects_xy(fish_geom, x_right_values, y)
| intersects_xy(fish_geom, x_values, y + cell_size_m)
| intersects_xy(fish_geom, x_right_values, y + cell_size_m)
)
if not np.any(mask):
y += cell_size_m
continue
profile["mask_row_hit"] += 1
if profile["mask_row_hit"] <= 20 or profile["mask_row_hit"] % 50 == 0:
log(
f"[TRACE] build_cells_for_geometry mask_hit "
f"row_idx={profile['row_checked']} candidate_mask={int(np.count_nonzero(mask))}"
)
mask = mask | np.r_[False, mask[:-1]] | np.r_[mask[1:], False]
candidate_indices = np.flatnonzero(mask)
profile["candidate_cells"] += int(candidate_indices.size)
if candidate_indices.size and (profile["candidate_cells"] <= 50 or profile["candidate_cells"] % 200 == 0):
log(
f"[TRACE] build_cells_for_geometry candidate_cells "
f"count={int(candidate_indices.size)} total={profile['candidate_cells']} "
f"row_y={y:.3f}"
)
for idx in candidate_indices:
profile["precise_cell_checks"] += 1
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
profile["precise_hits"] += 1
is_land = coast_prepared.intersects(cell_geom)
state_name = "LAND_BASE" if is_land else "SEA_SURFACE"
class_name = state_name
if is_land:
land_count += 1
else:
sea_count += 1
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)
export_bbox[0] = min(export_bbox[0], min_lon)
export_bbox[1] = min(export_bbox[1], min_lat)
export_bbox[2] = max(export_bbox[2], max_lon)
export_bbox[3] = max(export_bbox[3], max_lat)
ix = int(round(x / cell_size_m))
iy = int(round(y / cell_size_m))
cell_id = f"{ix}:{iy}"
rows.append(
(
LAYER_NAME,
cell_id,
iy,
ix,
cell_size_m,
state_name,
class_name,
source_name,
min_lon,
min_lat,
max_lon,
max_lat,
)
)
y += cell_size_m
if not rows:
log(
f"[TRACE] build_cells_for_geometry 结束 rows=0 land=0 sea=0 "
f"tile_hit={profile['tile_hit']}/{profile['tile_checked']} "
f"row_hit={profile['row_hit']}/{profile['row_checked']} "
f"candidate={profile['candidate_cells']} precise={profile['precise_hits']}/{profile['precise_cell_checks']}"
)
return [], 0, 0, (float("inf"), float("inf"), float("-inf"), float("-inf")), profile
log(
f"[TRACE] build_cells_for_geometry 结束 rows={len(rows)} land={land_count} sea={sea_count} "
f"tile_hit={profile['tile_hit']}/{profile['tile_checked']} "
f"row_hit={profile['row_hit']}/{profile['row_checked']} "
f"candidate={profile['candidate_cells']} precise={profile['precise_hits']}/{profile['precise_cell_checks']}"
)
return rows, land_count, sea_count, (export_bbox[0], export_bbox[1], export_bbox[2], export_bbox[3]), profile
tile_y = tile_start_y
while tile_y < tile_end_y:
tile_x = tile_start_x
tile_top = min(tile_y + scan_tile_m, end_y)
while tile_x < tile_end_x:
profile["tile_checked"] += 1
tile_right = min(tile_x + scan_tile_m, end_x)
tile_geom = box(tile_x, tile_y, tile_right, tile_top)
if not fish_prepared.intersects(tile_geom):
tile_x += scan_tile_m
continue
profile["tile_hit"] += 1
log(
f"[TRACE] build_cells_for_geometry tile_hit "
f"idx={profile['tile_checked']} bbox=({tile_x:.3f}, {tile_y:.3f}, {tile_right:.3f}, {tile_top:.3f})"
)
local_start_x = max(start_x, tile_x)
local_end_x = tile_right
local_start_y = max(start_y, tile_y)
local_end_y = tile_top
ix_start = int(round(local_start_x / cell_size_m))
ix_end = int(round(local_end_x / cell_size_m))
ix_values = np.arange(ix_start, ix_end, dtype=np.int64)
cell_count = int(ix_values.shape[0])
if cell_count == 0:
tile_x += scan_tile_m
continue
x_values = ix_values.astype(float) * cell_size_m
x_center_values = x_values + half
x_right_values = x_values + cell_size_m
y = local_start_y
while y < local_end_y:
profile["row_checked"] += 1
row_strip = box(local_start_x, y, local_end_x, y + cell_size_m)
if not fish_prepared.intersects(row_strip):
y += cell_size_m
continue
profile["row_hit"] += 1
if profile["row_hit"] <= 20 or profile["row_hit"] % 50 == 0:
log(
f"[TRACE] build_cells_for_geometry row_hit "
f"row_idx={profile['row_checked']} y={y:.3f} x_span=({local_start_x:.3f}, {local_end_x:.3f})"
)
mask = (
intersects_xy(fish_geom, x_center_values, y + half)
| intersects_xy(fish_geom, x_values, y)
| intersects_xy(fish_geom, x_right_values, y)
| intersects_xy(fish_geom, x_values, y + cell_size_m)
| intersects_xy(fish_geom, x_right_values, y + cell_size_m)
)
if not np.any(mask):
y += cell_size_m
continue
profile["mask_row_hit"] += 1
if profile["mask_row_hit"] <= 20 or profile["mask_row_hit"] % 50 == 0:
log(
f"[TRACE] build_cells_for_geometry mask_hit "
f"row_idx={profile['row_checked']} candidate_mask={int(np.count_nonzero(mask))}"
)
mask = mask | np.r_[False, mask[:-1]] | np.r_[mask[1:], False]
candidate_indices = np.flatnonzero(mask)
profile["candidate_cells"] += int(candidate_indices.size)
if candidate_indices.size and (profile["candidate_cells"] <= 50 or profile["candidate_cells"] % 200 == 0):
log(
f"[TRACE] build_cells_for_geometry candidate_cells "
f"count={int(candidate_indices.size)} total={profile['candidate_cells']} "
f"row_y={y:.3f}"
)
for idx in candidate_indices:
profile["precise_cell_checks"] += 1
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
profile["precise_hits"] += 1
is_land = coast_prepared.intersects(cell_geom)
state_name = "LAND_BASE" if is_land else "SEA_SURFACE"
class_name = state_name
if is_land:
land_count += 1
else:
sea_count += 1
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)
export_bbox[0] = min(export_bbox[0], min_lon)
export_bbox[1] = min(export_bbox[1], min_lat)
export_bbox[2] = max(export_bbox[2], max_lon)
export_bbox[3] = max(export_bbox[3], max_lat)
ix = int(round(x / cell_size_m))
iy = int(round(y / cell_size_m))
cell_id = f"{ix}:{iy}"
rows.append(
(
LAYER_NAME,
cell_id,
iy,
ix,
cell_size_m,
state_name,
class_name,
source_name,
min_lon,
min_lat,
max_lon,
max_lat,
)
)
y += cell_size_m
tile_x += scan_tile_m
if not rows:
log(
f"[TRACE] build_cells_for_geometry 结束 rows=0 land=0 sea=0 "
f"tile_hit={profile['tile_hit']}/{profile['tile_checked']} "
f"row_hit={profile['row_hit']}/{profile['row_checked']} "
f"candidate={profile['candidate_cells']} precise={profile['precise_hits']}/{profile['precise_cell_checks']}"
)
return [], 0, 0, (float("inf"), float("inf"), float("-inf"), float("-inf")), profile
log(
f"[TRACE] build_cells_for_geometry 结束 rows={len(rows)} land={land_count} sea={sea_count} "
f"tile_hit={profile['tile_hit']}/{profile['tile_checked']} "
f"row_hit={profile['row_hit']}/{profile['row_checked']} "
f"candidate={profile['candidate_cells']} precise={profile['precise_hits']}/{profile['precise_cell_checks']}"
)
return rows, land_count, sea_count, (export_bbox[0], export_bbox[1], export_bbox[2], export_bbox[3]), profile
@trace_fn("main")
def main() -> None:
parser = argparse.ArgumentParser(description="Rebuild nationwide fish-port 20m grid into MySQL with resume support")
parser.add_argument(
"--fish-source",
default=FISH_SRC,
help="渔港原始包 C09-06.zip",
)
parser.add_argument(
"--checkpoint-file",
default=DEFAULT_CHECKPOINT,
help="辅助断点文件(仅作记录)",
)
parser.add_argument(
"--current-prc-file",
default=DEFAULT_CURRENT_PRC_FILE,
help="当前运行 PRC 的状态文件",
)
parser.add_argument(
"--resume",
action="store_true",
default=True,
help="按数据库里的断点继续,默认开启;与 --prc 同用时会被忽略",
)
parser.add_argument(
"--no-resume",
dest="resume",
action="store_false",
help="忽略数据库断点,从头规划但不清表",
)
parser.add_argument(
"--reset-layer",
action="store_true",
help="先清空 fish_port_20m 的旧数据并删除断点",
)
parser.add_argument(
"--prc",
nargs="+",
default=None,
help="只处理指定 PRC指定时会按该 PRC 完整重建并忽略 resume 断点",
)
parser.add_argument(
"--fpc",
default=None,
help="只处理指定 FPC单个渔港指定时会先反查所在 PRC再只重算该港口",
)
parser.add_argument(
"--cluster-gap-m",
type=float,
default=250.0,
help="聚类连通判定的间隔(米)",
)
parser.add_argument(
"--cell-size-m",
type=float,
default=CELL_SIZE_M,
help="格网大小(米)",
)
parser.add_argument(
"--coast-buffer-m",
type=float,
default=COAST_BUFFER_M,
help="海岸缓冲距离(米)",
)
parser.add_argument(
"--fish-buffer-m",
type=float,
default=120.0,
help="渔港几何外扩缓冲(米),保留兼容参数;当前默认扫描以 coast_200m 粗格为底盘",
)
parser.add_argument(
"--commit-every",
type=int,
default=5000,
help="每累计多少条 cell 写一次数据库并打印状态",
)
parser.add_argument(
"--scan-tile-m",
type=float,
default=2000.0,
help="格网扫描分块大小(米)",
)
parser.add_argument(
"--coast-coarse-margin-m",
type=float,
default=DEFAULT_COAST_COARSE_MARGIN_M,
help="coast_200m 粗筛底座外扩缓冲(米),默认 0m精确按同区域粗格筛选",
)
parser.add_argument(
"--allow-unbounded-fallback",
action="store_true",
help="允许在没有 coast_200m 小窗时回退扫描原始渔港 part默认关闭以避免超大范围拖僵系统",
)
parser.add_argument(
"--skip-missing-coarse",
action="store_true",
help="遇到没有有效 coast_200m 交集的 part 时跳过;默认作为异常中止,避免静默漏算",
)
parser.add_argument(
"--max-scan-cells",
type=int,
default=200000,
help="单次 build_cells_for_geometry 允许估算扫描的最大 20m 格数,超过即中止;设为 0 表示不限制",
)
args = parser.parse_args()
max_scan_cells = None if args.max_scan_cells <= 0 else args.max_scan_cells
project_root = Path(__file__).resolve().parent.parent
fish_source = Path(args.fish_source)
if not fish_source.is_absolute():
fish_source = project_root / fish_source
if not fish_source.exists():
raise SystemExit(f"missing input: {fish_source}")
checkpoint_path = Path(args.checkpoint_file)
if not checkpoint_path.is_absolute():
checkpoint_path = project_root / checkpoint_path
current_prc_path = Path(args.current_prc_file)
if not current_prc_path.is_absolute():
current_prc_path = project_root / current_prc_path
ensure_database()
conn = mysql_connect(DB_NAME)
try:
ensure_schema(conn)
coast_coarse_geom, coast_coarse_bbox, coast_coarse_count = load_coarse_mask(conn, COAST_COARSE_LAYER)
if coast_coarse_geom is not None and coast_coarse_count > 0:
if args.coast_coarse_margin_m > 0:
coast_coarse_geom = coast_coarse_geom.buffer(
args.coast_coarse_margin_m,
cap_style=2,
join_style=2,
)
coast_coarse_prepared = prep(coast_coarse_geom)
log(
f"[{LAYER_NAME}] coast_200m 粗筛底座就绪cells={coast_coarse_count} "
f"bbox={coast_coarse_bbox} margin={args.coast_coarse_margin_m:.1f}m"
)
else:
coast_coarse_geom = None
coast_coarse_prepared = None
if args.allow_unbounded_fallback:
log(f"[{LAYER_NAME}] coast_200m 粗筛底座为空,已允许回退按原始渔港范围扫描")
elif args.skip_missing_coarse:
log(f"[{LAYER_NAME}] coast_200m 粗筛底座为空,已允许跳过无粗筛窗口的 part")
else:
log(f"[{LAYER_NAME}] coast_200m 粗筛底座为空,后续遇到无粗筛窗口的 part 将作为异常中止")
selected_prc = None if args.prc is None else {normalize_prc_token(str(x)) for x in args.prc}
selected_fpc = normalize_fpc_token(args.fpc) if args.fpc else None
if selected_fpc is not None and args.resume:
log(f"[{LAYER_NAME}] 指定 FPC 模式,忽略 checkpoint 续跑,改为按指定渔港完整重建")
args.resume = False
if selected_prc is not None:
if args.resume:
log(f"[{LAYER_NAME}] 指定 PRC 模式,忽略 checkpoint 续跑,改为按指定 PRC 完整重建")
args.resume = False
log(f"[{LAYER_NAME}] 指定 PRC{', '.join(sorted(selected_prc))}")
drop_existing_prcs(conn, LAYER_NAME, selected_prc)
if selected_fpc is not None:
log(f"[{LAYER_NAME}] 指定 FPC{selected_fpc}")
if args.reset_layer:
log(f"[{LAYER_NAME}] 清空旧数据并删除 checkpoint ...")
drop_existing_layer(conn, LAYER_NAME)
if checkpoint_path.exists():
checkpoint_path.unlink()
if current_prc_path.exists():
current_prc_path.unlink()
checkpoint = load_resume_state(conn, checkpoint_path, RESUME_JOB_NAME, LAYER_NAME) if args.resume else None
completed_prcs = set(normalize_prc_list(checkpoint.get("completed_prcs", []))) if checkpoint else set()
if checkpoint:
log(
f"[{LAYER_NAME}] 读取断点:"
f"last_prc={checkpoint.get('last_prc')} "
f"completed={len(completed_prcs)} "
f"current_prc={checkpoint.get('current_prc')}"
)
log(f"[{LAYER_NAME}] 开始解析渔港源:{fish_source}")
root = load_xml(fish_source)
point_lookup = build_point_lookup(root)
prc_list = discover_target_prcs(root, selected_prc, selected_fpc)
if not prc_list:
if selected_fpc is not None:
raise SystemExit(f"no fish-port line features selected for FPC={selected_fpc}")
raise SystemExit("no fish-port line features selected")
if selected_fpc is not None:
log(f"[{LAYER_NAME}] 选中 PRC 数量:{len(prc_list)}(由 FPC 反查得到)")
else:
log(f"[{LAYER_NAME}] 选中 PRC 数量:{len(prc_list)}")
total_inserted = 0
total_land = 0
total_sea = 0
global_cluster_index = 0
started_at = time.monotonic()
checkpoint_state = checkpoint or {
"job_name": RESUME_JOB_NAME,
"layer_name": LAYER_NAME,
"source_name": str(fish_source),
"status": "running",
"last_prc": None,
"last_cluster_index": -1,
"last_part_index": -1,
"processed_clusters": 0,
"inserted_cells": 0,
"land_cells": 0,
"sea_cells": 0,
"current_prc": None,
"current_prc_started_at": None,
"completed_prcs": [],
"updated_at": None,
}
resume_prc = checkpoint.get("current_prc") if checkpoint else None
if resume_prc in completed_prcs:
resume_prc = None
resume_prc_index = prc_list.index(resume_prc) if resume_prc in prc_list else None
if args.resume and selected_prc is None:
pending_prcs = [
prc
for prc in prc_list
if prc not in completed_prcs or prc == resume_prc
]
if not pending_prcs:
log(f"[{LAYER_NAME}] 所有 PRC 都已完成resume 不需要继续")
return
prc_list = pending_prcs
resume_prc_index = prc_list.index(resume_prc) if resume_prc in prc_list else None
with conn.cursor() as cur:
for prc_index, prc in enumerate(prc_list):
if selected_prc is not None and prc not in selected_prc:
continue
if args.resume and selected_prc is None and prc in completed_prcs and prc != resume_prc:
continue
if resume_prc_index is not None and prc_index < resume_prc_index:
continue
if resume_prc is not None and prc == resume_prc and checkpoint and checkpoint.get("last_cluster_index", -1) >= 0:
log(
f"[{LAYER_NAME}] PRC={prc} 从断点恢复,"
f"last_cluster_index={checkpoint.get('last_cluster_index')}, "
f"last_part_index={checkpoint.get('last_part_index', -1)}"
)
prc_lines = collect_fish_lines_for_prc(root, point_lookup, prc, selected_fpc=selected_fpc)
if not prc_lines:
if selected_fpc is not None:
log(f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} 没有渔港线要素,跳过")
else:
log(f"[{LAYER_NAME}] PRC={prc} 没有渔港线要素,跳过")
continue
if selected_fpc is not None:
log(
f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} 开始,"
f"线要素={len(prc_lines)},已累计={checkpoint_state['inserted_cells']}"
)
else:
log(f"[{LAYER_NAME}] PRC={prc} 开始,线要素={len(prc_lines)},已累计={checkpoint_state['inserted_cells']}")
checkpoint_state.update(
{
"status": "running",
"current_prc": prc,
"current_prc_started_at": dt.datetime.now().isoformat(timespec="seconds"),
"completed_prcs": normalize_prc_list(completed_prcs),
"updated_at": dt.datetime.now().isoformat(timespec="seconds"),
}
)
save_checkpoint(checkpoint_path, checkpoint_state)
write_prc_status(
current_prc_path,
{
"job_name": RESUME_JOB_NAME,
"layer_name": LAYER_NAME,
"status": "running",
"current_prc": prc,
"current_prc_started_at": checkpoint_state["current_prc_started_at"],
"last_prc": checkpoint_state.get("last_prc"),
"completed_prcs": normalize_prc_list(completed_prcs),
"updated_at": checkpoint_state["updated_at"],
**({"fpc": selected_fpc} if selected_fpc is not None else {}),
},
)
if selected_fpc is not None:
log(f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} 准备按单港 bbox 裁切海岸线并做缓冲 ...")
else:
log(f"[{LAYER_NAME}] PRC={prc} 准备按单港 bbox 裁切海岸线并做缓冲 ...")
prc_bboxes = [line.bbox for line in prc_lines]
prc_bbox = (
min(bb[0] for bb in prc_bboxes),
min(bb[1] for bb in prc_bboxes),
max(bb[2] for bb in prc_bboxes),
max(bb[3] for bb in prc_bboxes),
)
cluster_groups = cluster_line_indices(prc_bboxes, gap_m=args.cluster_gap_m)
if selected_fpc is not None:
log(f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} cluster 数={len(cluster_groups)}")
else:
log(f"[{LAYER_NAME}] PRC={prc} cluster 数={len(cluster_groups)}")
if selected_fpc is not None:
log(f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} 正在缓存整港海岸线并生成缓冲区 ...")
else:
log(f"[{LAYER_NAME}] PRC={prc} 正在缓存整县海岸线并生成缓冲区 ...")
prc_coast_lines, prc_coast_bbox = load_coastlines_for_prc(
project_root,
prc,
bbox=(
mercator_x(prc_bbox[0]),
mercator_y(prc_bbox[1]),
mercator_x(prc_bbox[2]),
mercator_y(prc_bbox[3]),
),
search_margin_m=max(args.coast_buffer_m, args.coast_coarse_margin_m),
)
if selected_fpc is not None:
log(
f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} 海岸线缓存完成,"
f"曲线数={len(prc_coast_lines)} bbox={prc_coast_bbox}"
)
else:
log(
f"[{LAYER_NAME}] PRC={prc} 海岸线缓存完成,"
f"曲线数={len(prc_coast_lines)} bbox={prc_coast_bbox}"
)
prc_coast_geom = unary_union(prc_coast_lines)
prc_coast_buffer = prc_coast_geom.buffer(args.coast_buffer_m, cap_style=2, join_style=2)
prc_coast_prepared = prep(prc_coast_buffer)
if selected_fpc is not None:
log(f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} 海岸缓冲缓存完成,开始处理各 part ...")
else:
log(f"[{LAYER_NAME}] PRC={prc} 海岸缓冲缓存完成,开始处理各 part ...")
resume_cluster = 0
if checkpoint and checkpoint.get("last_prc") == prc:
resume_cluster = int(checkpoint.get("last_cluster_index", -1)) + 1
prc_started_at = time.monotonic()
prc_inserted_before = checkpoint_state["inserted_cells"]
prc_land_before = checkpoint_state["land_cells"]
prc_sea_before = checkpoint_state["sea_cells"]
selected_port_rows: dict[str, tuple] | None = {} if selected_fpc is not None else None
selected_port_coarse_windows: set[tuple[float, float, float, float]] | None = set() if selected_fpc is not None else None
prc_profile = {
"tile_checked": 0,
"tile_hit": 0,
"row_checked": 0,
"row_hit": 0,
"mask_row_hit": 0,
"candidate_cells": 0,
"precise_cell_checks": 0,
"precise_hits": 0,
}
for cluster_idx, cluster in enumerate(cluster_groups):
global_cluster_index += 1
if checkpoint and checkpoint.get("last_prc") == prc and cluster_idx < resume_cluster:
continue
log(f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1}/{len(cluster_groups)} 开始,成员={len(cluster)}")
log(f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1} 正在合并渔港线 ...")
cluster_lines = [
LineString([(mercator_x(lon), mercator_y(lat)) for lon, lat in prc_lines[i].coords])
for i in cluster
]
merged = unary_union(cluster_lines)
polygons = list(polygonize(merged))
if polygons:
fish_geom = unary_union(polygons)
geom_mode = "polygonize"
else:
fish_geom = merged.buffer(args.cell_size_m, cap_style=2, join_style=2)
geom_mode = "buffer"
if fish_geom.is_empty:
log(f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx} 为空,跳过")
continue
log(f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1} 几何模式={geom_mode},正在生成格网 ...")
fish_parts = [part for part in get_parts(fish_geom) if not part.is_empty]
if not fish_parts:
log(f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1} 没有可拆分部件,跳过")
continue
resume_part = 0
if checkpoint and checkpoint.get("last_prc") == prc and checkpoint.get("last_cluster_index") == cluster_idx:
resume_part = int(checkpoint.get("last_part_index", -1)) + 1
for part_idx, fish_part in enumerate(fish_parts):
if checkpoint and checkpoint.get("last_prc") == prc and checkpoint.get("last_cluster_index") == cluster_idx and part_idx < resume_part:
continue
fish_bbox = fish_part.bounds
coarse_windows = load_coarse_windows_for_bbox(
conn,
layer_name=COAST_COARSE_LAYER,
bbox_mercator=fish_bbox,
margin_m=args.coast_coarse_margin_m,
)
scan_windows: list[object] = []
if coarse_windows:
log(
f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1}/{len(cluster_groups)} "
f"part={part_idx + 1}/{len(fish_parts)} 取回 coast_200m 格子={len(coarse_windows)}"
)
for coarse_window in coarse_windows:
window_geom = box(*coarse_window)
scan_geom = fish_part.intersection(window_geom)
if not scan_geom.is_empty:
scan_windows.append(scan_geom)
if selected_port_coarse_windows is not None:
selected_port_coarse_windows.add(tuple(float(v) for v in coarse_window))
if not scan_windows:
if args.allow_unbounded_fallback:
scan_windows = [fish_part]
log(
f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1}/{len(cluster_groups)} "
f"part={part_idx + 1}/{len(fish_parts)} 没有有效 coast_200m 交集,已允许回退原始范围"
)
else:
message = (
f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1}/{len(cluster_groups)} "
f"part={part_idx + 1}/{len(fish_parts)} 没有有效 coast_200m 交集;"
f"fish_bbox={tuple(round(v, 3) for v in fish_bbox)} "
f"coarse_windows={len(coarse_windows)}"
)
if args.skip_missing_coarse:
log(message + "已按 --skip-missing-coarse 跳过")
continue
raise RuntimeError(
message
+ "按当前设计这属于异常:渔港 20m 必须以数据库 coast_200m 为底盘。"
+ "如确需旧式整片扫描请显式加 --allow-unbounded-fallback"
+ "如确认允许漏算该 part请显式加 --skip-missing-coarse。"
)
else:
log(
f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1}/{len(cluster_groups)} "
f"part={part_idx + 1}/{len(fish_parts)} 实际扫描交集窗口={len(scan_windows)}"
)
if selected_fpc is not None:
log(
f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} cluster={cluster_idx + 1}/{len(cluster_groups)} "
f"part={part_idx + 1}/{len(fish_parts)} bbox={fish_bbox} 使用海岸线缓存 ..."
)
else:
log(
f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1}/{len(cluster_groups)} "
f"part={part_idx + 1}/{len(fish_parts)} bbox={fish_bbox} 使用 PRC 级海岸线缓存 ..."
)
coast_prepared = prc_coast_prepared
log(f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1} part={part_idx + 1} 开始格网 ...")
land_count = 0
sea_count = 0
bbox_lonlat = (float("inf"), float("inf"), float("-inf"), float("-inf"))
profile = {key: 0 for key in prc_profile}
part_inserted = 0
batch: list[tuple] = []
for window_idx, scan_geom in enumerate(scan_windows):
if scan_geom.is_empty:
continue
log(
f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1}/{len(cluster_groups)} "
f"part={part_idx + 1}/{len(fish_parts)} window={window_idx + 1}/{len(scan_windows)} "
f"正在生成格网 ..."
)
window_rows, window_land, window_sea, window_bbox, window_profile = build_cells_for_geometry(
scan_geom,
coast_prepared,
prc=prc,
cell_size_m=args.cell_size_m,
scan_tile_m=args.cell_size_m * 10.0,
source_name=(
f"C09-06+coastline PRC={prc}"
+ (f" FPC={selected_fpc}" if selected_fpc is not None else "")
),
max_scan_cells=max_scan_cells,
)
land_count += window_land
sea_count += window_sea
bbox_lonlat = (
min(bbox_lonlat[0], window_bbox[0]),
min(bbox_lonlat[1], window_bbox[1]),
max(bbox_lonlat[2], window_bbox[2]),
max(bbox_lonlat[3], window_bbox[3]),
)
for key in prc_profile:
prc_profile[key] += window_profile[key]
profile[key] += window_profile[key]
for row in window_rows:
if selected_port_rows is not None:
selected_port_rows[row[1]] = row
batch.append(row)
if len(batch) >= args.commit_every:
if selected_fpc is not None:
delete_cells_by_ids(conn, LAYER_NAME, [item[1] for item in batch])
insert_cells(cur, batch)
conn.commit()
part_inserted += len(batch)
total_inserted += len(batch)
checkpoint_state["inserted_cells"] += len(batch)
checkpoint_state["updated_at"] = dt.datetime.now().isoformat(timespec="seconds")
log(
f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1}/{len(cluster_groups)} "
f"part={part_idx + 1}/{len(fish_parts)} "
f"提交={part_inserted} 累计={checkpoint_state['inserted_cells']} "
f"land={checkpoint_state['land_cells']} sea={checkpoint_state['sea_cells']}"
)
batch.clear()
if batch:
if selected_fpc is not None:
delete_cells_by_ids(conn, LAYER_NAME, [item[1] for item in batch])
insert_cells(cur, batch)
conn.commit()
part_inserted += len(batch)
total_inserted += len(batch)
checkpoint_state["inserted_cells"] += len(batch)
checkpoint_state["updated_at"] = dt.datetime.now().isoformat(timespec="seconds")
log(
f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1}/{len(cluster_groups)} "
f"part={part_idx + 1}/{len(fish_parts)} "
f"提交={part_inserted} 累计={checkpoint_state['inserted_cells']} "
f"land={checkpoint_state['land_cells']} sea={checkpoint_state['sea_cells']}"
)
if part_inserted == 0:
log(
f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1} "
f"part={part_idx + 1} 没有生成 cell"
)
checkpoint_state.update(
{
"last_prc": prc,
"last_cluster_index": cluster_idx,
"last_part_index": part_idx,
"processed_clusters": checkpoint_state["processed_clusters"] + 1,
"updated_at": dt.datetime.now().isoformat(timespec="seconds"),
}
)
save_checkpoint(checkpoint_path, checkpoint_state)
continue
if selected_fpc is not None:
log(
f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} cluster={cluster_idx + 1} "
f"part={part_idx + 1} 格网完成cell={part_inserted} land={land_count} sea={sea_count} bbox={bbox_lonlat}"
)
log(
f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} cluster={cluster_idx + 1} "
f"part={part_idx + 1} 写库完成"
)
else:
log(
f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1} "
f"part={part_idx + 1} 格网完成cell={part_inserted} land={land_count} sea={sea_count} bbox={bbox_lonlat}"
)
log(f"[{LAYER_NAME}] PRC={prc} cluster={cluster_idx + 1} part={part_idx + 1} 写库完成")
checkpoint_state.update(
{
"last_prc": prc,
"last_cluster_index": cluster_idx,
"last_part_index": part_idx,
"processed_clusters": checkpoint_state["processed_clusters"] + 1,
"land_cells": checkpoint_state["land_cells"] + land_count,
"sea_cells": checkpoint_state["sea_cells"] + sea_count,
"current_prc": prc,
"completed_prcs": normalize_prc_list(completed_prcs | {prc}),
"updated_at": dt.datetime.now().isoformat(timespec="seconds"),
}
)
save_checkpoint(checkpoint_path, checkpoint_state)
write_prc_status(
current_prc_path,
{
"job_name": RESUME_JOB_NAME,
"layer_name": LAYER_NAME,
"status": "running",
"current_prc": prc,
"current_prc_started_at": checkpoint_state.get("current_prc_started_at"),
"last_prc": prc,
"last_cluster_index": cluster_idx,
"last_part_index": part_idx,
"completed_prcs": normalize_prc_list(completed_prcs | {prc}),
"inserted_cells": checkpoint_state["inserted_cells"],
"land_cells": checkpoint_state["land_cells"],
"sea_cells": checkpoint_state["sea_cells"],
"updated_at": checkpoint_state["updated_at"],
**({"fpc": selected_fpc} if selected_fpc is not None else {}),
},
)
total_land += land_count
total_sea += sea_count
prc_elapsed = time.monotonic() - prc_started_at
prc_inserted_now = checkpoint_state["inserted_cells"] - prc_inserted_before
prc_land_now = checkpoint_state["land_cells"] - prc_land_before
prc_sea_now = checkpoint_state["sea_cells"] - prc_sea_before
summary_inserted = len(selected_port_rows) if selected_port_rows is not None else prc_inserted_now
summary_coarse = len(selected_port_coarse_windows) if selected_port_coarse_windows is not None else 0
insert_progress(
cur,
job_name="fish_port_20m_full_mysql_resume",
layer_name=LAYER_NAME,
prc=prc,
status_name="FPC_DONE" if selected_fpc is not None else "PRC_DONE",
cluster_count=len(cluster_groups),
inserted_cells=summary_inserted,
land_cells=prc_land_now,
sea_cells=prc_sea_now,
elapsed_sec=prc_elapsed,
)
conn.commit()
cur.execute(
"""
INSERT INTO navsea_grid_import_state
(job_name, layer_name, source_name, checkpoint_json)
VALUES
(%s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
layer_name=VALUES(layer_name),
source_name=VALUES(source_name),
checkpoint_json=VALUES(checkpoint_json)
""",
(
RESUME_JOB_NAME,
LAYER_NAME,
str(fish_source),
json.dumps(checkpoint_state, ensure_ascii=False),
),
)
conn.commit()
if selected_fpc is not None:
log(
f"[{LAYER_NAME}] PRC={prc} / FPC={selected_fpc} 完成 cluster={len(cluster_groups)} "
f"200x200={summary_coarse} final_20x20={summary_inserted} "
f"land={prc_land_now} sea={prc_sea_now} elapsed={prc_elapsed:.1f}s"
)
else:
log(
f"[{LAYER_NAME}] PRC={prc} 完成 cluster={len(cluster_groups)} "
f"本段写入={prc_inserted_now} land={prc_land_now} sea={prc_sea_now} "
f"elapsed={prc_elapsed:.1f}s"
)
log(
f"[{LAYER_NAME}] PRC={prc} profiling "
f"tile={prc_profile['tile_hit']}/{prc_profile['tile_checked']} "
f"row={prc_profile['row_hit']}/{prc_profile['row_checked']} "
f"mask_row={prc_profile['mask_row_hit']} "
f"candidate={prc_profile['candidate_cells']} "
f"precise={prc_profile['precise_hits']}/{prc_profile['precise_cell_checks']}"
)
completed_prcs.add(prc)
checkpoint_state["completed_prcs"] = normalize_prc_list(completed_prcs)
checkpoint_state["status"] = "idle"
checkpoint_state["current_prc"] = prc
checkpoint_state["current_prc_started_at"] = checkpoint_state.get("current_prc_started_at")
checkpoint_state["updated_at"] = dt.datetime.now().isoformat(timespec="seconds")
save_checkpoint(checkpoint_path, checkpoint_state)
write_prc_status(
current_prc_path,
{
"job_name": RESUME_JOB_NAME,
"layer_name": LAYER_NAME,
"status": "done",
"current_prc": prc,
"current_prc_started_at": checkpoint_state.get("current_prc_started_at"),
"completed_prcs": normalize_prc_list(completed_prcs),
"finished_at": checkpoint_state["updated_at"],
"updated_at": checkpoint_state["updated_at"],
**({"fpc": selected_fpc, "coarse_200m_intersections": summary_coarse, "final_20x20_cells": summary_inserted} if selected_fpc is not None else {}),
},
)
del prc_lines
cur.execute("SELECT COUNT(*) FROM navsea_grid_cell WHERE layer_name=%s", (LAYER_NAME,))
final_count = int(cur.fetchone()[0])
cur.execute(
"""
SELECT MIN(min_lon), MIN(min_lat), MAX(max_lon), MAX(max_lat)
FROM navsea_grid_cell
WHERE layer_name=%s
""",
(LAYER_NAME,),
)
bbox_row = cur.fetchone()
bbox = (
float(bbox_row[0]) if bbox_row and bbox_row[0] is not None else float("inf"),
float(bbox_row[1]) if bbox_row and bbox_row[1] is not None else float("inf"),
float(bbox_row[2]) if bbox_row and bbox_row[2] is not None else float("-inf"),
float(bbox_row[3]) if bbox_row and bbox_row[3] is not None else float("-inf"),
)
upsert_meta(
cur,
LAYER_NAME,
"全国渔港 20m 可航/不可航格",
"coastline/C09-06.zip + navsea_japan_coast_grid.coast_200m",
args.cell_size_m,
final_count,
bbox,
None,
)
cur.execute(
"""
INSERT INTO navsea_grid_import_state
(job_name, layer_name, source_name, checkpoint_json)
VALUES
(%s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
layer_name=VALUES(layer_name),
source_name=VALUES(source_name),
checkpoint_json=VALUES(checkpoint_json)
""",
(
RESUME_JOB_NAME,
LAYER_NAME,
str(fish_source),
json.dumps(checkpoint_state, ensure_ascii=False),
),
)
conn.commit()
log(f"[{LAYER_NAME}] 完成checkpoint={checkpoint_path}")
finally:
conn.close()
if __name__ == "__main__":
main()