from __future__ import annotations import os from dataclasses import dataclass import pymysql @dataclass(frozen=True) class DbConfig: host: str = os.getenv("NAVSEA_DB_HOST", "localhost") port: int = int(os.getenv("NAVSEA_DB_PORT", "3306")) user: str = os.getenv("NAVSEA_DB_USER", "root") password: str = os.getenv("NAVSEA_DB_PASSWORD", "2chi9ks2") database: str = os.getenv("NAVSEA_DB_NAME", "pbf_analysis") unix_socket: str | None = os.getenv("NAVSEA_DB_SOCKET", "/tmp/mysql.sock") FAMILY_DEFAULT_CAPABILITIES = { "hazard": {"collision"}, "navigation_aid": {"navigation_mark"}, "bathymetry": {"depth_reference"}, "seabed": {"bottom_reference"}, "fishery": {"entangle"}, "route": {"route_reference"}, "boundary": {"boundary_reference"}, "anchorage": {"anchorage_reference"}, "restricted_area": {"boundary_reference"}, "infrastructure": {"structure_reference"}, "utility": {"utility_hazard"}, "landmark": {"landmark_reference"}, "place": {"place_reference"}, "water_area": {"water_reference"}, "monitoring": {"traffic_monitoring"}, "overlay_support": {"render_support"}, "unknown": {"review_required"}, } class DetectionPipeline: def __init__(self, config: DbConfig) -> None: self.config = config def connect(self): kwargs = { "host": self.config.host, "port": self.config.port, "user": self.config.user, "password": self.config.password, "database": self.config.database, "charset": "utf8mb4", "autocommit": False, } if self.config.unix_socket and self.config.host in {"localhost", "127.0.0.1"}: kwargs["unix_socket"] = self.config.unix_socket return pymysql.connect(**kwargs) def run(self) -> None: with self.connect() as conn: with conn.cursor() as cur: object_types = self.fetch_object_types(cur) taxonomy_rows = [self.classify_object_type(name) for name in object_types] capability_rows = self.build_capabilities(taxonomy_rows) self.create_taxonomy_table(cur, taxonomy_rows) self.create_capabilities_table(cur, capability_rows) self.create_detection_view(cur) self.create_capability_views(cur) self.ensure_query_indexes(cur) conn.commit() print( "Built navsea_object_taxonomy, navsea_object_capabilities, " "navsea_detection_objects, capability views and supporting query indexes" ) @staticmethod def fetch_object_types(cur) -> list[str]: cur.execute( """ SELECT canonical_object_type FROM canonical_object_rules ORDER BY canonical_object_type """ ) return [row[0] for row in cur.fetchall()] @staticmethod def is_depth_band(name: str) -> bool: compact = name.replace(" ", "") return compact.endswith("m") or "m以深" in compact @staticmethod def classify_object_type(name: str) -> tuple[str, str, str, str]: if name.startswith(("P", "L", "p")) and ( name.endswith("ククリ") or name in { "P穴", "P陸域", "P危険界ククリ", "P投錨注意障害物ククリ", "P施設・境界線等ククリ", "P航路ククリ", "P錨泊地等ククリ", "P754ククリ", "P721ククリ", "P730ククリ", "L海底地形", "p地名", "p地名陸", "p高さ制限", } ): return ( name, "overlay_support", "render_support", "保留原始 source_layer 的叠加支持对象,不作为独立语义实体替换原始渲染层。", ) if DetectionPipeline.is_depth_band(name): return (name, "bathymetry", "depth_zone", "水深分带对象,用于浅滩、深度范围和搁浅风险分析。") if any(token in name for token in ("等深線",)): return (name, "bathymetry", "depth_contour", "等深线对象,用于水深变化与航线安全分析。") if any(token in name for token in ("底質", "海底地形", "サンドウェーブ", "海底火山")): return (name, "seabed", "seabed_feature", "海底形态或底质对象,用于海底环境识别。") if any( token in name for token in ( "険悪物", "障害物", "危険物", "危険全沈没船", "沈船", "全沈没船", "沈木", "暗岩", "洗岩", "干出岩", "水上岩", "孤立危険物", "魚礁", "浅所危険界", "撤去跡", "掃海済み", "サンゴ礁", ) ): return (name, "hazard", "obstacle", "对航行存在碰撞或搁浅风险的危险物对象。") if any( token in name for token in ( "灯", "灯台", "灯標", "灯浮標", "浮標", "立標", "導灯", "指向灯", "V-AIS", "管制信号所", ) ): return (name, "navigation_aid", "navigation_mark", "航标、灯标或导助航对象。") if any(token in name for token in ("航路", "進路矢印", "分離通航方式", "誘導線", "指導線")): return (name, "route", "route_reference", "航路、导向线或通航组织对象。") if any( token in name for token in ( "境界", "危険界", "境界線", "制限区域", "専用用途海域", "航泊禁止区域", "錨泊禁止区域", "航空機進入区域", ) ): return (name, "boundary", "boundary_control", "边界、限制区或规则控制对象。") if "錨泊" in name or "錨地" in name: return (name, "anchorage", "anchorage", "锚地或锚泊控制对象。") if any(token in name for token in ("漁", "養殖場", "漁網", "海草")): return (name, "fishery", "fishery_area", "渔业、养殖或缠绕风险相关对象。") if any( token in name for token in ( "防波堤", "潜提", "桟橋", "海上バース", "ドルフィン", "ポンツーン", "橋", "道路", "ビル", "タンク", "煙突", "塔", "やぐら", "ケーソン", "土砂捨て場", "石油開発台", ) ): return (name, "infrastructure", "structure", "港口、岸线或陆上海工构造物对象。") if any(token in name for token in ("海底線", "輸送管", "送電線", "架空線", "放水口", "取水口")): return (name, "utility", "utility_line", "海底线缆、管道或公用设施对象。") if any(token in name for token in ("河川域", "湖沼域", "陸上水域", "干潮帯", "未測海域", "平水境界")): return (name, "water_area", "water_area", "水域、潮滩或未测区域对象。") if any( token in name for token in ( "その他 (記念碑等)", "陸上顕著物", "地名", "山頂", "港湾", "漁港", "マリーナ", "フィッシャリーナ", "海の駅", "漁業協同組合", "海事関係署", "税関", "パイロットステーション", ) ): return (name, "place", "place_label", "地名、显著地物或港区服务设施类参考对象。") if any(token in name for token in ("廃棄物捨て場", "貯木場")): return (name, "infrastructure", "structure", "作业、堆置或弃置区域类设施对象。") if any(token in name for token in ("無線局", "レーダー局")): return (name, "monitoring", "monitoring_station", "通信、雷达或监测设施对象。") if any(token in name for token in ("急潮", "波紋", "激潮", "渦流", "水源方向")): return (name, "hazard", "current_hazard", "流态、水流或潮流风险相关对象。") return (name, "unknown", "review_required", "尚未细化归类的对象,需要人工复核。") @staticmethod def build_capabilities( taxonomy_rows: list[tuple[str, str, str, str]] ) -> list[tuple[str, str]]: capability_rows: set[tuple[str, str]] = set() for canonical_object_type, object_family, detection_class, _ in taxonomy_rows: capabilities = set(FAMILY_DEFAULT_CAPABILITIES.get(object_family, {"review_required"})) if detection_class in {"obstacle"}: capabilities.add("collision") if detection_class in {"depth_zone", "depth_contour"}: capabilities.add("grounding") if "魚礁" in canonical_object_type or "浅所" in canonical_object_type or "干潮帯" in canonical_object_type: capabilities.add("grounding") if any(token in canonical_object_type for token in ("漁", "養殖場", "漁網", "海草")): capabilities.add("entangle") if any(token in canonical_object_type for token in ("防波堤",)): capabilities.add("wave_barrier") if any(token in canonical_object_type for token in ("架空線", "送電線", "高さ制限")): capabilities.add("overhead_clearance") if any(token in canonical_object_type for token in ("航路", "誘導線", "進路矢印", "分離通航方式")): capabilities.add("route_reference") if any(token in canonical_object_type for token in ("錨泊", "錨地")): capabilities.add("anchorage_reference") if any(token in canonical_object_type for token in ("境界", "区域", "危険界")): capabilities.add("boundary_reference") if any(token in canonical_object_type for token in ("海底線", "輸送管")): capabilities.add("snag_risk") if any(token in canonical_object_type for token in ("灯", "標", "浮標", "導灯", "V-AIS", "管制信号所")): capabilities.add("navigation_mark") if object_family in {"place", "infrastructure", "monitoring"}: capabilities.add("landmark_reference") for capability in sorted(capabilities): capability_rows.add((canonical_object_type, capability)) return sorted(capability_rows) @staticmethod def create_taxonomy_table(cur, rows: list[tuple[str, str, str, str]]) -> None: cur.execute("DROP TABLE IF EXISTS navsea_object_taxonomy") cur.execute( """ CREATE TABLE navsea_object_taxonomy ( canonical_object_type VARCHAR(191) NOT NULL, object_family VARCHAR(64) NOT NULL, detection_class VARCHAR(64) NOT NULL, description TEXT NULL, PRIMARY KEY (canonical_object_type), KEY idx_family_class (object_family, detection_class) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """ ) cur.executemany( """ INSERT INTO navsea_object_taxonomy ( canonical_object_type, object_family, detection_class, description ) VALUES (%s, %s, %s, %s) """, rows, ) @staticmethod def create_capabilities_table(cur, rows: list[tuple[str, str]]) -> None: cur.execute("DROP TABLE IF EXISTS navsea_object_capabilities") cur.execute( """ CREATE TABLE navsea_object_capabilities ( canonical_object_type VARCHAR(191) NOT NULL, capability VARCHAR(64) NOT NULL, PRIMARY KEY (canonical_object_type, capability), KEY idx_capability (capability, canonical_object_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """ ) cur.executemany( """ INSERT INTO navsea_object_capabilities ( canonical_object_type, capability ) VALUES (%s, %s) """, rows, ) @staticmethod def create_detection_view(cur) -> None: cur.execute("DROP VIEW IF EXISTS navsea_detection_objects") cur.execute( """ CREATE VIEW navsea_detection_objects AS SELECT r.feature_id, CAST(NULL AS CHAR(1)) AS geometry, r.z, r.x, r.y, r.geom_type, r.source_layer, r.class_name, r.shape_name, r.layer_name, t.canonical_object_type, t.object_family, t.detection_class, c.capability, r.detection_key FROM pbf_relayer_candidates r JOIN navsea_object_taxonomy t ON r.canonical_object_type = t.canonical_object_type LEFT JOIN navsea_object_capabilities c ON r.canonical_object_type = c.canonical_object_type """ ) @staticmethod def create_capability_views(cur) -> None: views = { "navsea_collision_objects": "collision", "navsea_grounding_objects": "grounding", "navsea_entangle_objects": "entangle", "navsea_navigation_mark_objects": "navigation_mark", "navsea_route_reference_objects": "route_reference", "navsea_boundary_reference_objects": "boundary_reference", } for view_name, capability in views.items(): cur.execute(f"DROP VIEW IF EXISTS {view_name}") cur.execute( f""" CREATE VIEW {view_name} AS SELECT * FROM navsea_detection_objects WHERE capability = %s """, (capability,), ) @staticmethod def add_index_if_missing(cur, table: str, index_name: str, ddl: str) -> None: cur.execute(f"SHOW INDEX FROM {table} WHERE Key_name=%s", (index_name,)) if cur.fetchone() is None: cur.execute(ddl) def ensure_query_indexes(self, cur) -> None: self.add_index_if_missing( cur, "pbf_relayer_candidates", "idx_navsea_tile", "ALTER TABLE pbf_relayer_candidates ADD KEY idx_navsea_tile (z, x, y)", ) self.add_index_if_missing( cur, "pbf_relayer_candidates", "idx_navsea_object", "ALTER TABLE pbf_relayer_candidates ADD KEY idx_navsea_object (canonical_object_type(100), geom_type)", ) self.add_index_if_missing( cur, "pbf_relayer_candidates", "idx_navsea_detection_key", "ALTER TABLE pbf_relayer_candidates ADD KEY idx_navsea_detection_key (detection_key(64))", ) self.add_index_if_missing( cur, "pbf_relayer_candidates", "idx_navsea_source_layer", "ALTER TABLE pbf_relayer_candidates ADD KEY idx_navsea_source_layer (source_layer(100), geom_type)", ) def main() -> None: DetectionPipeline(DbConfig()).run() if __name__ == "__main__": main()