#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ CM95 唐津 AOI Presence Diagnostic v2 只读诊断: 1) chart_cell bbox 是否覆盖唐津 z10/881/410; 2) chart_feature bbox 是否命中; 3) 实际 Point/Line/Area geometry 是否通过 MBRIntersects 命中; 4) Sounding point 是否命中; 5) 输出离唐津中心最近的 Cell / Feature,帮助判断 Dataset 1 是否根本没有唐津细节数据。 不修改数据库。 """ from __future__ import annotations import argparse import getpass import json import math from pathlib import Path from datetime import datetime import pymysql from pymysql.cursors import DictCursor LON_MIN = 129.7265625 LAT_MIN = 33.4314413355753 LON_MAX = 130.078125 LAT_MAX = 33.7243396617476 CENTER_LON = (LON_MIN + LON_MAX) / 2 CENTER_LAT = (LAT_MIN + LAT_MAX) / 2 def args(): p = argparse.ArgumentParser() p.add_argument("--host", default="127.0.0.1") p.add_argument("--port", type=int, default=3306) p.add_argument("--user", default="root") p.add_argument("--password") p.add_argument("--database", default="cm93_chart") p.add_argument("--dataset-id", type=int, default=1) p.add_argument("--output", default="cm95_karatsu_presence_diagnostic_v2.json") return p.parse_args() def fetch(cur, sql, params=()): cur.execute(sql, params) return list(cur.fetchall()) def scalar(cur, sql, params=()): cur.execute(sql, params) r = cur.fetchone() return None if not r else next(iter(r.values())) def main(): a = args() if a.password is None: a.password = getpass.getpass("MySQL password: ") conn = pymysql.connect( host=a.host, port=a.port, user=a.user, password=a.password, database=a.database, charset="utf8mb4", cursorclass=DictCursor, autocommit=False, connect_timeout=20, read_timeout=900, ) report = { "version": "v2", "generated_at": datetime.now().isoformat(timespec="seconds"), "dataset_id": a.dataset_id, "target": { "tile": "z10/881/410", "bbox": [LON_MIN, LAT_MIN, LON_MAX, LAT_MAX], "center": [CENTER_LON, CENTER_LAT], }, "read_only": True, } poly_wkt = ( f"POLYGON(({LON_MIN} {LAT_MIN}," f"{LON_MAX} {LAT_MIN}," f"{LON_MAX} {LAT_MAX}," f"{LON_MIN} {LAT_MAX}," f"{LON_MIN} {LAT_MIN}))" ) try: with conn.cursor() as cur: try: cur.execute("SET SESSION TRANSACTION READ ONLY") cur.execute("START TRANSACTION READ ONLY") except Exception: cur.execute("START TRANSACTION") report["dataset"] = fetch(cur, """ SELECT id,name,source_type,source_root,parser_version,normalized_srid FROM chart_dataset WHERE id=%s """, (a.dataset_id,)) report["global_feature_bbox"] = fetch(cur, """ SELECT MIN(bbox_min_lon) min_lon, MIN(bbox_min_lat) min_lat, MAX(bbox_max_lon) max_lon, MAX(bbox_max_lat) max_lat, SUM(bbox_min_lon IS NOT NULL) bbox_rows, COUNT(*) total_rows FROM chart_feature WHERE dataset_id=%s """, (a.dataset_id,)) report["global_cell_bbox"] = fetch(cur, """ SELECT MIN(bbox_min_lon) min_lon, MIN(bbox_min_lat) min_lat, MAX(bbox_max_lon) max_lon, MAX(bbox_max_lat) max_lat, SUM(bbox_min_lon IS NOT NULL) bbox_rows, COUNT(*) total_rows FROM chart_cell WHERE dataset_id=%s """, (a.dataset_id,)) bbox_params = ( a.dataset_id, LON_MIN, LON_MAX, LAT_MIN, LAT_MAX, ) report["cell_bbox_overlap"] = fetch(cur, """ SELECT id,source_relative_path,scale_code,native_scale, bbox_min_lon,bbox_min_lat,bbox_max_lon,bbox_max_lat, feature_count FROM chart_cell WHERE dataset_id=%s AND bbox_max_lon >= %s AND bbox_min_lon <= %s AND bbox_max_lat >= %s AND bbox_min_lat <= %s ORDER BY native_scale,source_relative_path """, bbox_params) report["feature_bbox_count"] = scalar(cur, """ SELECT COUNT(*) FROM chart_feature WHERE dataset_id=%s AND bbox_max_lon >= %s AND bbox_min_lon <= %s AND bbox_max_lat >= %s AND bbox_min_lat <= %s """, bbox_params) # Direct actual geometry checks. These bypass chart_feature bbox metadata. direct = {} for kind, table in [ ("point", "chart_point_geometry"), ("line", "chart_line_geometry"), ("area", "chart_area_geometry"), ]: try: direct[kind] = scalar(cur, f""" SELECT COUNT(*) FROM {table} g JOIN chart_feature f ON f.id=g.feature_id WHERE f.dataset_id=%s AND MBRIntersects( g.geom, ST_GeomFromText(%s,4326) ) """, (a.dataset_id, poly_wkt)) except Exception as e: direct[kind] = {"error": str(e)} report["direct_geometry_mbr_counts"] = direct try: report["sounding_bbox_count"] = scalar(cur, """ SELECT COUNT(*) FROM chart_sounding_point s JOIN chart_feature f ON f.id=s.feature_id WHERE f.dataset_id=%s AND ST_X(s.geom) BETWEEN %s AND %s AND ST_Y(s.geom) BETWEEN %s AND %s """, ( a.dataset_id, LON_MIN, LON_MAX, LAT_MIN, LAT_MAX, )) except Exception as e: report["sounding_bbox_count"] = {"error": str(e)} # Nearest cell by bbox center, excluding pathological world-scale cells when possible. report["nearest_cells"] = fetch(cur, """ SELECT id,source_relative_path,scale_code,native_scale, bbox_min_lon,bbox_min_lat,bbox_max_lon,bbox_max_lat, feature_count, SQRT( POW(((bbox_min_lon+bbox_max_lon)/2)-%s,2) + POW(((bbox_min_lat+bbox_max_lat)/2)-%s,2) ) AS center_distance_deg FROM chart_cell WHERE dataset_id=%s AND bbox_min_lon IS NOT NULL AND bbox_min_lat IS NOT NULL AND bbox_max_lon IS NOT NULL AND bbox_max_lat IS NOT NULL AND (bbox_max_lon-bbox_min_lon) < 20 AND (bbox_max_lat-bbox_min_lat) < 20 ORDER BY center_distance_deg ASC LIMIT 30 """, (CENTER_LON, CENTER_LAT, a.dataset_id)) report["nearest_features"] = fetch(cur, """ SELECT f.id,f.cell_id,c.source_relative_path, f.source_record_index,f.object_class_id,f.object_class, f.geometry_type,f.scale_code,f.native_scale, f.bbox_min_lon,f.bbox_min_lat,f.bbox_max_lon,f.bbox_max_lat, SQRT( POW(((f.bbox_min_lon+f.bbox_max_lon)/2)-%s,2) + POW(((f.bbox_min_lat+f.bbox_max_lat)/2)-%s,2) ) AS center_distance_deg FROM chart_feature f JOIN chart_cell c ON c.id=f.cell_id WHERE f.dataset_id=%s AND f.bbox_min_lon IS NOT NULL AND f.bbox_min_lat IS NOT NULL AND f.bbox_max_lon IS NOT NULL AND f.bbox_max_lat IS NOT NULL ORDER BY center_distance_deg ASC LIMIT 50 """, (CENTER_LON, CENTER_LAT, a.dataset_id)) # Coarse 2-degree spatial occupancy by feature bbox center. report["coarse_feature_grid"] = fetch(cur, """ SELECT FLOOR(((bbox_min_lon+bbox_max_lon)/2)/2)*2 AS lon_bin, FLOOR(((bbox_min_lat+bbox_max_lat)/2)/2)*2 AS lat_bin, COUNT(*) AS feature_count FROM chart_feature WHERE dataset_id=%s AND bbox_min_lon IS NOT NULL AND bbox_min_lat IS NOT NULL AND bbox_max_lon IS NOT NULL AND bbox_max_lat IS NOT NULL AND ((bbox_min_lon+bbox_max_lon)/2) BETWEEN 120 AND 160 AND ((bbox_min_lat+bbox_max_lat)/2) BETWEEN 20 AND 50 GROUP BY lon_bin,lat_bin ORDER BY lat_bin,lon_bin """, (a.dataset_id,)) conn.rollback() finally: conn.close() # Derive diagnosis. fcnt = report.get("feature_bbox_count") d = report.get("direct_geometry_mbr_counts", {}) direct_sum = 0 direct_errors = [] for k, v in d.items(): if isinstance(v, int): direct_sum += v else: direct_errors.append((k, v)) if isinstance(fcnt, int) and fcnt > 0: diagnosis = "AOI_PRESENT_BY_FEATURE_BBOX" elif direct_sum > 0: diagnosis = "AOI_PRESENT_IN_GEOMETRY_BUT_FEATURE_BBOX_QUERY_FAILED" else: diagnosis = "NO_CM95_GEOMETRY_FOUND_IN_KARATSU_AOI" report["diagnosis"] = diagnosis report["direct_geometry_total"] = direct_sum report["direct_geometry_errors"] = direct_errors out = Path(a.output).resolve() out.write_text(json.dumps(report, ensure_ascii=False, indent=2, default=str), encoding="utf-8") print("CM95 唐津 AOI Presence Diagnostic v2") print("=" * 72) print("Target bbox :", [LON_MIN, LAT_MIN, LON_MAX, LAT_MAX]) print("Cell bbox overlap :", len(report["cell_bbox_overlap"])) print("Feature bbox count:", report["feature_bbox_count"]) print("Direct geometry :", report["direct_geometry_mbr_counts"]) print("Soundings :", report["sounding_bbox_count"]) print("Diagnosis :", diagnosis) print("") print("Nearest cells:") for r in report["nearest_cells"][:10]: print( f" {r['source_relative_path']} scale={r['native_scale']} " f"bbox=({r['bbox_min_lon']},{r['bbox_min_lat']}," f"{r['bbox_max_lon']},{r['bbox_max_lat']}) " f"dist={float(r['center_distance_deg']):.3f}°" ) print("") print("Output:", out) print("把 JSON 上传给 ChatGPT。") if __name__ == "__main__": main()